<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Fabian on Software Engineering</title><description>Ship fast. Sleep well. Pragmatic lessons on building software, from a dev who has suffered too much.</description><link>https://letsch.dev/</link><language>en-us</language><item><title>Tailwind is Great. Tailwind Sucks.</title><link>https://letsch.dev/blog/tailwind-is-great-tailwind-sucks/</link><guid isPermaLink="true">https://letsch.dev/blog/tailwind-is-great-tailwind-sucks/</guid><description>Tailwind CSS pros and cons after 3 years of professional use: real trade-offs, noisy markup, speed, consistency, and when to use Tailwind.</description><pubDate>Sat, 30 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve seen Tailwind dismissed as unreadable garbage and praised as the best thing since sliced bread. After three years of professional use, I think both sides are equally right and mostly talking past each other.&lt;/p&gt;
&lt;p&gt;The opposition is often arguing based on taste: developers look at a wall of utility classes and get a funny feeling in their stomach. And yet, advocates rarely make a convincing case for why that discomfort is worth it. There are many good pro and contra arguments for Tailwind and I think both camps could benefit by putting emotions aside and acknowledging its trade-offs.&lt;/p&gt;
&lt;p&gt;I reach for it when it fits, and leave it when it doesn’t, and I think you should, too. I’ll share how to tell if it’s the right call, but let’s first go through the pros and cons of Tailwind to establish a baseline.&lt;/p&gt;
&lt;h2 id=&quot;whats-so-good-about-tailwind&quot;&gt;&lt;a href=&quot;#whats-so-good-about-tailwind&quot;&gt;What’s so good about Tailwind&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;locality-of-behavior&quot;&gt;&lt;a href=&quot;#locality-of-behavior&quot;&gt;Locality of Behavior&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Imagine you’re buying groceries, and you want to know the price of an item. With conventional CSS, the price would be on a shelf tag, hopefully fairly close to the product. You have to look at the item, then look somewhere else to get the information you need.&lt;/p&gt;
&lt;p&gt;Tailwind takes a different approach: the price is printed directly on the packaging. If you look at the item, you immediately know everything you need to know about it, no second location required.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/_astro/tailwind-is-great-tailwind-sucks-2.DKNPm-2h_ZyIrnn.webp&quot; alt=&quot;Minimal cereal shelf with fictional boxes and number-only price stickers on each package&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1536&quot; height=&quot;485&quot;&gt;&lt;/p&gt;
&lt;p&gt;The engineering principle applied here is called &lt;strong&gt;locality of behavior&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Tailwind allows you to style elements directly in your markup, so you don’t have to dig through stylesheets or jump between CSS and HTML sections, which can be confusing and time-consuming, even if they’re in the same file.&lt;/p&gt;
&lt;h3 id=&quot;naming-no-more&quot;&gt;&lt;a href=&quot;#naming-no-more&quot;&gt;Naming no more&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Phil Karlton once said: “There are only two hard things in Computer Science: cache invalidation and naming things.”&lt;/p&gt;
&lt;p&gt;Thankfully, CSS itself doesn’t mess with caching, so there is nothing to invalidate. It however involves lots of naming when applying classes to elements, forcing you to pretend that a box added purely for presentation is a concept worthy of a meaningful name. Consider this example:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes catppuccin-mocha catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;--shiki-dark-bg:#1e1e2e;color:#cdd6f4;--shiki-dark:#cdd6f4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#F9E2AF;--shiki-dark:#F9E2AF&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;quot;field&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;    &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#F9E2AF;--shiki-dark:#F9E2AF&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;quot;field__label&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt;Search&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;label&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;--shiki-light-font-style:italic;--shiki-dark:#9399B2;--shiki-dark-font-style:italic&quot;&gt;    &amp;lt;!-- What should we name the following div? --&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;    &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#F9E2AF;--shiki-dark:#F9E2AF&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;quot;???&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;        &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;input&lt;/span&gt;&lt;span style=&quot;color:#F9E2AF;--shiki-dark:#F9E2AF&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;quot;field__input&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;        &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;svg&lt;/span&gt;&lt;span style=&quot;color:#F9E2AF;--shiki-dark:#F9E2AF&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;quot;field__icon&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;svg&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;    &amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no value in naming the div with the question marks. In fact giving it a name somehow implies that it is meaningful, but in reality it just exists to add some flex properties.&lt;/p&gt;
&lt;p&gt;With Tailwind you can sidestep this waste of time and brain-power. Since the classes in Tailwind &lt;em&gt;are&lt;/em&gt; the styles, there is nothing left to invent. Just apply the utility classes you need and move on.&lt;/p&gt;
&lt;h3 id=&quot;natural-consistency&quot;&gt;&lt;a href=&quot;#natural-consistency&quot;&gt;Natural Consistency&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Tailwind makes consistency easier than regular CSS by giving you a predefined token system for spacing, typography, colors, shadows, border radius, and more.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;p-4&lt;/code&gt;, &lt;code&gt;rounded-lg&lt;/code&gt;, or &lt;code&gt;opacity-75&lt;/code&gt; feels natural because they’re the defaults. Going off script with something like &lt;code&gt;p-[5px]&lt;/code&gt; is possible, but it feels wrong. That little bit of friction is what keeps people and coding agents aligned without blocking them, naturally reducing drift.&lt;/p&gt;
&lt;h3 id=&quot;zero-specificity-headaches&quot;&gt;&lt;a href=&quot;#zero-specificity-headaches&quot;&gt;Zero Specificity Headaches&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Using Tailwind, most of the time you won’t have to deal with CSS selector complexities, including specificity. You don’t have to mess with ids, nested selectors, nth-child or element selectors, as Tailwind only uses classes for everything and classes all have the same specificity.&lt;/p&gt;
&lt;p&gt;And if you use &lt;code&gt;tailwind-merge&lt;/code&gt;, then you can reduce the ambiguity of “What class will be applied here?” as well, as you can ensure that always the last class that is mentioned for a given property is applied:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes catppuccin-mocha catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;--shiki-dark-bg:#1e1e2e;color:#cdd6f4;--shiki-dark:#cdd6f4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic&quot;&gt;twMerge&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;#39;text-md text-sm text-lg text-xs&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-dark:#9399B2&quot;&gt;;&lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-light-font-style:italic;--shiki-dark:#9399B2;--shiki-dark-font-style:italic&quot;&gt; // text-xs will be applied as it is the last one.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;minimal-production-css&quot;&gt;&lt;a href=&quot;#minimal-production-css&quot;&gt;Minimal Production CSS&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Tailwind keeps your production CSS minimal. Its build process scans the codebase for used Tailwind classes and removes any it didn’t find from the final bundle. This reduces the output drastically for most projects, going down from multiple megabytes of tailwind classes, to a few kilobytes.&lt;/p&gt;
&lt;h2 id=&quot;what-sucks-about-tailwind&quot;&gt;&lt;a href=&quot;#what-sucks-about-tailwind&quot;&gt;What Sucks about Tailwind&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;classes-classes-classes-src-classes&quot;&gt;&lt;a href=&quot;#classes-classes-classes-src-classes&quot;&gt;Classes, Classes, Classes, Src, Classes&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I mentioned earlier how convenient it is to read the price right off the item. That works beautifully for just a price. But in reality, HTML elements are packed with more information. Tailwind classes and div-soup can suffocate important attributes in your markup like &lt;code&gt;alt&lt;/code&gt;, &lt;code&gt;src&lt;/code&gt; or &lt;code&gt;aria-label&lt;/code&gt; and make it an unreadable mess, turning the beloved locality of behavior into a &lt;strong&gt;locality of noise.&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes catppuccin-mocha catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;--shiki-dark-bg:#1e1e2e;color:#cdd6f4;--shiki-dark:#cdd6f4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;jsx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;--shiki-light-font-style:italic;--shiki-dark:#9399B2;--shiki-dark-font-style:italic&quot;&gt;// Is this image lazy loaded or not?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;img&lt;/span&gt;&lt;span style=&quot;color:#F9E2AF;--shiki-dark:#F9E2AF&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;quot;organic-chicken.jpg&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#F9E2AF;--shiki-dark:#F9E2AF&quot;&gt; alt&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;quot;Free-range organic chicken&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F9E2AF;--shiki-dark:#F9E2AF&quot;&gt;className&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;quot;block w-full h-48 md:h-64 object-cover rounded-t-xl&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;border-2 border-amber-300 bg-white p-0.5 shadow-lg hover:shadow-2xl&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;hover:scale-105 transition-all duration-200 ease-out dark:border-amber-600&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;dark:bg-gray-800&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#F9E2AF;--shiki-dark:#F9E2AF&quot;&gt; loading&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;quot;lazy&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#F9E2AF;--shiki-dark:#F9E2AF&quot;&gt; decoding&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;quot;async&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;its-complex-and-inconsistent&quot;&gt;&lt;a href=&quot;#its-complex-and-inconsistent&quot;&gt;It’s Complex And Inconsistent&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Tailwind promises to reduce cognitive load, but actually introduces lots of it. &lt;strong&gt;The upfront learning curve is steep.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One would wish that they just used CSS namings for everything, but they instead named line-height &lt;code&gt;leading&lt;/code&gt; and letter-spacing &lt;code&gt;tracking&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But Tailwind doesn’t stop with weird namings. They also introduced things like square bracket syntax for arbitrary values (&lt;code&gt;p-[5px]&lt;/code&gt;), negative prefixes that look like CLI flags (&lt;code&gt;-mx-4&lt;/code&gt;), bang-syntax for !important (&lt;code&gt;!font-bold&lt;/code&gt;), and magic &lt;code&gt;group&lt;/code&gt; and &lt;code&gt;peer&lt;/code&gt; modifiers that force you to tag parents or siblings to react to their state. All of these features are useful and important, but every one of these is its own micro-syntax you have to memorize, turning what should be simple styling into a sea of additional complexities you have to learn.&lt;/p&gt;
&lt;h3 id=&quot;its-a-leaky-abstraction&quot;&gt;&lt;a href=&quot;#its-a-leaky-abstraction&quot;&gt;It’s a Leaky Abstraction&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;People getting into Tailwind often think they’ll never write &lt;code&gt;style=&amp;quot;&amp;quot;&lt;/code&gt; again. That is sadly not true. There are plenty of situations where using classes simply isn’t possible. For example, the Tailwind compiler only ships classes it actually sees in your codebase, so a dynamic class like &lt;code&gt;h-[${elementHeight}px]&lt;/code&gt; won’t work. You’re forced to reach for the &lt;code&gt;style&lt;/code&gt; attribute, or worse, a safe-list workaround where you maintain a string with all the possible values it could have, just to trick the scanner:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes catppuccin-mocha catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;--shiki-dark-bg:#1e1e2e;color:#cdd6f4;--shiki-dark:#cdd6f4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;javascript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;--shiki-light-font-style:italic;--shiki-dark:#9399B2;--shiki-dark-font-style:italic&quot;&gt;// h-[1px], h-[2px], h-[3px], h-[4px], ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And it also isn’t up-to-date with the newest CSS features.&lt;/p&gt;
&lt;p&gt;At the bottom of this post you can find a scroll-driven “Copy link” button animation that, at time of writing, only works in Chromium-based browsers. If I had used Tailwind, I would have had to write a plugin or custom CSS, to get this behavior, dealing with additional complexities or leaving the benefits of the abstraction behind.&lt;/p&gt;
&lt;h3 id=&quot;dependency-overhead&quot;&gt;&lt;a href=&quot;#dependency-overhead&quot;&gt;Dependency Overhead&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;You will probably not just install &lt;code&gt;tailwindcss&lt;/code&gt; and &lt;code&gt;@tailwindcss/vite&lt;/code&gt;, but also packages like &lt;code&gt;tailwind-merge&lt;/code&gt; or &lt;code&gt;@tailwindcss/typography&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Tailwind comes with dependencies. This probably isn’t a big deal for you, but maybe it’s a good idea to have fewer dependencies if we have a new npm worm like every two weeks.&lt;/p&gt;
&lt;h3 id=&quot;build-step-required&quot;&gt;&lt;a href=&quot;#build-step-required&quot;&gt;Build Step Required&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Tailwind needs a build step. Now, if you work with something like React, then this will be a non-issue. But let’s say you are working in a weird web-page builder plugin inside Wordpress and you are editing some markup directly. Using Tailwind wouldn’t work in that environment without introducing other major drawbacks.&lt;/p&gt;
&lt;h2 id=&quot;when-to-use-tailwind&quot;&gt;&lt;a href=&quot;#when-to-use-tailwind&quot;&gt;When to use Tailwind&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;All in all I don’t think Tailwind is perfect, but I also don’t think it’s bad. There are situations where its use is valid and there are situations where it isn’t. Now that we’ve got the pros and cons out of the way, let’s talk about when to use it.&lt;/p&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;&lt;a href=&quot;#prerequisites&quot;&gt;Prerequisites&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A few preconditions should be met before I would even consider it.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Being able to have a build step is mandatory, because otherwise you will have to ship the entirety of Tailwind, which for newer versions isn’t even possible and older versions require like ~3 Megabytes of CSS. For comparison: this whole site including HTML, CSS, Javascript and images is less than 300kb.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Being able to create components is very helpful, which means you want to be using a framework like React, Vue or Astro. Without access to components, you will end up either copying a lot of classes, breaking the engineering principle “Don’t Repeat Yourself”, or you will end up breaking the Tailwind philosophy and creating classes like &lt;code&gt;btn&lt;/code&gt; or &lt;code&gt;input&lt;/code&gt;, which basically nullify most of the benefits of using Tailwind in the first place.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;speed--perfectionism&quot;&gt;&lt;a href=&quot;#speed--perfectionism&quot;&gt;Speed &amp;gt; Perfectionism&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This site doesn’t use Tailwind. The main reason for that is that i am over-engineering it. This is neither sane nor necessary, but this website represents me and the values i hold and i dearly love good UX, good quality and good performance.&lt;/p&gt;
&lt;p&gt;I am using many weird features: scroll animations, variable media queries, a weird old monitor overlay, uncommon CSS selectors… All of this is possible to some extent with Tailwind, but it would feel like trying to thread a needle while wearing gloves. It’s possible, but I would much rather do it bare-handed.&lt;/p&gt;
&lt;p&gt;Now if you are not like me, and you do normal stuff while simultaneously trying to deliver results quickly, then Tailwind would probably be the better choice for most situations because not having to name things and also not having to find related styles to your markup saves a lot of time, even if the messiness is a real pain.&lt;/p&gt;
&lt;h3 id=&quot;team-work&quot;&gt;&lt;a href=&quot;#team-work&quot;&gt;Team Work&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I think teams, especially big ones, should also consider using Tailwind. As stated earlier, it naturally leads towards consistency, which is a bigger challenge, the bigger the team and product gets. Tailwinds limited options are a major blessing in such an environment.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Tailwind isn’t a hero and it isn’t a villain. It’s a trade: noisy markup in exchange for speed and consistency. Use it if that trade makes sense under your constraints, but don’t force it if it doesn’t. The best tool is the one that fits the job, not the one that fits your identity.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;I’d genuinely love to hear your thoughts. Please drop your feedback on &lt;a href=&quot;https://app.daily.dev/posts/cpiw3eonj&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;daily.dev&lt;/a&gt;, &lt;a href=&quot;https://www.linkedin.com/feed/update/urn:li:activity:7466609487525605377/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;LinkedIn&lt;/a&gt; or &lt;a href=&quot;https://indieweb.social/@anoian/116665810776239194&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;Mastodon&lt;/a&gt;!&lt;/p&gt;</content:encoded><wordCount>1536</wordCount><updated>2026-06-18T21:01:14.000Z</updated><category>bestpractices</category><category>mindset</category><author>Fabian Letsch</author><enclosure url="https://letsch.dev/_astro/tailwind-is-great-tailwind-sucks.B9ncaoHX.webp" length="4325376" type="image/webp"/></item><item><title>Linters: The Most Underabused Tool In The AI-Slop Era</title><link>https://letsch.dev/blog/linters-the-most-underabused-tool-in-the-ai-slop-era/</link><guid isPermaLink="true">https://letsch.dev/blog/linters-the-most-underabused-tool-in-the-ai-slop-era/</guid><description>AI-generated code is fast but inconsistent. Learn how linters eliminate repetitive review issues, enforce standards, and keep your codebase clean, predictable, and scalable in the AI era.</description><pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you are generating a lot of code with AI and nobody on your team is actively maintaining your linting rules, you are wasting review time and slowly letting AI-generated garbage creep into your codebase.&lt;/p&gt;
&lt;p&gt;AI is fast. Ridiculously fast. It will generate components, utilities, hooks, tests, whatever you ask for, in seconds. But it’s also inconsistent. It ignores conventions, reinvents patterns, and subtly drifts over time. Even when it’s “good,” it’s not reliably aligned with your codebase.&lt;/p&gt;
&lt;p&gt;And code review? It simply cannot keep up with that pace.&lt;/p&gt;
&lt;p&gt;So you need something that pushes back. Not sometimes. Not when a reviewer catches it. Every time.&lt;/p&gt;
&lt;p&gt;You need linters.&lt;/p&gt;
&lt;h2 id=&quot;use-linters-for-more-than-parentheses-or-quotes&quot;&gt;&lt;a href=&quot;#use-linters-for-more-than-parentheses-or-quotes&quot;&gt;Use Linters For More Than Parentheses or Quotes&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;There’s a simple rule of thumb:&lt;/p&gt;
&lt;p&gt;If you can describe a problem precisely, you can probably enforce it with a linter.&lt;/p&gt;
&lt;p&gt;I’ve implemented a lot of custom lint rules across different projects. And the pattern is always the same: anything that keeps showing up in reviews can usually be automated away.&lt;/p&gt;
&lt;p&gt;Let’s start with a category that you might not think of when it comes to linting.&lt;/p&gt;
&lt;h2 id=&quot;translations--i18n&quot;&gt;&lt;a href=&quot;#translations--i18n&quot;&gt;Translations &amp;amp; i18n&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you have JSON or YAML translation files in your codebase, then you will know the following problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Translation keys get referenced that don’t exist&lt;/li&gt;
&lt;li&gt;Strings get hardcoded directly into the HTML&lt;/li&gt;
&lt;li&gt;Translation keys accumulate over time that are not used anymore&lt;/li&gt;
&lt;li&gt;Formal and informal pronouns are mixed in languages like German or French (“Du” vs “Sie”)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Inconsistent UX&lt;/li&gt;
&lt;li&gt;Broken translations in production&lt;/li&gt;
&lt;li&gt;Endless, repetitive review comments&lt;/li&gt;
&lt;li&gt;A file with 5000 lines that could be half the size if you would just know which keys are used and which aren’t.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of this is preventable with a linter.&lt;/p&gt;
&lt;h3 id=&quot;what-good-lint-rules-can-enforce&quot;&gt;&lt;a href=&quot;#what-good-lint-rules-can-enforce&quot;&gt;What good lint rules can enforce&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Tone consistency in translations.&lt;br/&gt;
You can scan translation files and forbid specific phrases. If your product uses “Sie,” then “Du” becomes a lint error.&lt;/p&gt;
&lt;p&gt;Or hardcoded user-facing strings. Write a lint rule to prevent untranslated code.&lt;/p&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes catppuccin-mocha catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;--shiki-dark-bg:#1e1e2e;color:#cdd6f4;--shiki-dark:#cdd6f4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt;Welcome back&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Good:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes catppuccin-mocha catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;--shiki-dark-bg:#1e1e2e;color:#cdd6f4;--shiki-dark:#cdd6f4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-dark:#9399B2&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic&quot;&gt;t&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;#39;welcome_back&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-dark:#9399B2&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#89B4FA;--shiki-dark:#89B4FA&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A linter can flag every hardcoded string in templates and force everything through your translation layer, even aria-label, title, and alt.&lt;/p&gt;
&lt;p&gt;Also invalid translation keys.&lt;br/&gt;
You can statically verify that keys actually exist.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes catppuccin-mocha catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;--shiki-dark-bg:#1e1e2e;color:#cdd6f4;--shiki-dark:#cdd6f4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#89B4FA;--shiki-light-font-style:italic;--shiki-dark:#89B4FA;--shiki-dark-font-style:italic&quot;&gt;t&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt;&amp;#39;dashboard.welccome&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-dark:#9399B2&quot;&gt;;&lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-light-font-style:italic;--shiki-dark:#9399B2;--shiki-dark-font-style:italic&quot;&gt; // typo&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should fail immediately, not in production, not in QA.&lt;/p&gt;
&lt;h2 id=&quot;enforcing-specific-imports&quot;&gt;&lt;a href=&quot;#enforcing-specific-imports&quot;&gt;Enforcing Specific Imports&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Another subtle problem: having multiple ways to do the same thing. Imagine you’re using lodash in your project, but you’ve also implemented some of its functions yourself for one reason or another.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes catppuccin-mocha catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;--shiki-dark-bg:#1e1e2e;color:#cdd6f4;--shiki-dark:#cdd6f4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;--shiki-light-font-style:italic;--shiki-dark:#9399B2;--shiki-dark-font-style:italic&quot;&gt;// ❌&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7;--shiki-dark:#CBA6F7&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-dark:#9399B2&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt; debounce &lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-dark:#9399B2&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#CBA6F7;--shiki-dark:#CBA6F7&quot;&gt; from&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt; &amp;#39;lodash&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-dark:#9399B2&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;--shiki-light-font-style:italic;--shiki-dark:#9399B2;--shiki-dark-font-style:italic&quot;&gt;// ✅&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7;--shiki-dark:#CBA6F7&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-dark:#9399B2&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt; debounce &lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-dark:#9399B2&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#CBA6F7;--shiki-dark:#CBA6F7&quot;&gt; from&lt;/span&gt;&lt;span style=&quot;color:#A6E3A1;--shiki-dark:#A6E3A1&quot;&gt; &amp;#39;@/utils/debounce&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-dark:#9399B2&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both are technically valid, and imports rarely get much attention in code reviews, so the wrong one tends to creep in over time.&lt;/p&gt;
&lt;p&gt;AI will usually pick whichever option it has seen more often, or just make up something that looks reasonable.&lt;/p&gt;
&lt;p&gt;But with a linter, you can ban specific imports. And with autofixers, you don’t even have to rely on developers to correct them manually.&lt;/p&gt;
&lt;h2 id=&quot;other-high-leverage-rules&quot;&gt;&lt;a href=&quot;#other-high-leverage-rules&quot;&gt;Other High-Leverage Rules&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Once you think about it, opportunities are everywhere:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ban entire libraries (e.g., &lt;code&gt;moment&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Enforce architectural boundaries&lt;br/&gt;
→ Nothing in &lt;code&gt;shared/&lt;/code&gt; can import from &lt;code&gt;features/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Enforce domain-specific naming&lt;br/&gt;
→ Import icons from the icon library reliably with the &lt;code&gt;...Icon&lt;/code&gt; suffix.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You should be creative with this. Think about what problems you have regularly.&lt;/p&gt;
&lt;p&gt;And then you can even task LLMs to write the rules for you. They are exceptionally good at it, as linter rules are isolated from the rest of the codebase.&lt;/p&gt;
&lt;h2 id=&quot;the-antidote-for-curser--entropyc-puns-intended&quot;&gt;&lt;a href=&quot;#the-antidote-for-curser--entropyc-puns-intended&quot;&gt;The Antidote for Curser &amp;amp; Entropyc (Puns Intended)&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You might argue that you are using AI code reviews to solve these issues. And I agree, you should absolutely configure automatic AI code reviews and also build a proper agentic engineering setup. These things help. A lot.&lt;/p&gt;
&lt;p&gt;But AI does not follow its prompts 100% of the time, no matter how good the prompt or context.&lt;/p&gt;
&lt;p&gt;Linters are different. They either pass or fail. Every time.&lt;/p&gt;
&lt;p&gt;Therefore, I highly suggest to go all-in on both. AI code reviews with custom prompts are especially powerful when your expectations are easy to explain, but hard to express in code.&lt;/p&gt;
&lt;p&gt;Why have only one silver bullet if you could have two?&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Linters won’t replace engineering judgment.&lt;br/&gt;
They won’t replace code reviews. (Human &amp;amp; AI)&lt;/p&gt;
&lt;p&gt;But they will eliminate entire categories of problems before they ever reach review.&lt;/p&gt;
&lt;p&gt;And that’s the point.&lt;/p&gt;
&lt;p&gt;If you’re using AI heavily and not evolving your lint rules alongside it, you’re choosing to fight the same problems manually, over and over again.&lt;/p&gt;
&lt;p&gt;Or you automate them once, and move on.&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;bonus-configuration-best-practices&quot;&gt;&lt;a href=&quot;#bonus-configuration-best-practices&quot;&gt;Bonus: Configuration Best Practices&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you hate linters, you might have configured them wrong.&lt;/p&gt;
&lt;p&gt;The rule is simple: fix issues where they happen, not later.&lt;/p&gt;
&lt;p&gt;Linters belong in your editor, including autofixes on save. If AI writes code, it should follow the same rule and lint immediately. Anything else is delayed feedback. Make sure not to run the linter on the entire project here, just the related files.&lt;/p&gt;
&lt;p&gt;Pre-commit and pre-push hooks sound useful, but they often punish fast workflows. Frequent commits are a good thing, so they shouldn’t become painful.&lt;/p&gt;
&lt;p&gt;The CI in pull requests should run the linter as well. This is your safety net for anything that was overlooked locally.&lt;/p&gt;
&lt;p&gt;Get this right, and linting fades into the background. Fast, predictable, and just doing its job.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;I’d genuinely love to hear your thoughts. Please drop your feedback on &lt;a href=&quot;https://app.daily.dev/posts/3ozoLHDZU&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;daily.dev&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/feed/update/urn:li:activity:7449949266355081216/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;LinkedIn&lt;/a&gt;!&lt;/p&gt;</content:encoded><wordCount>904</wordCount><updated>2026-05-01T22:38:47.000Z</updated><category>bestpractices</category><category>craftsmanship</category><author>Fabian Letsch</author><enclosure url="https://letsch.dev/_astro/linters-the-most-underabused-tool-in-the-ai-slop-era.YFgtQ1q6.webp" length="4325376" type="image/webp"/></item><item><title>Clean Code Is Dangerous</title><link>https://letsch.dev/blog/clean-code-is-a-lie/</link><guid isPermaLink="true">https://letsch.dev/blog/clean-code-is-a-lie/</guid><description>Clean Code taught us to value readability, but treating it as a rule instead of a tradeoff leads to worse outcomes. Engineering is about balance, not principles.</description><pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Few books have influenced everyday software development as much as &lt;em&gt;Clean Code&lt;/em&gt;. It taught a generation of engineers to value readability, naming, small functions, and clarity. I learned from it too, and, like many others, I try to apply its principles if I can.&lt;/p&gt;
&lt;p&gt;But the broader message behind it is often taken too far. Readability is one important goal in software engineering, but it is not the only one. It has to be balanced against other constraints. Once that balance is turned into a rule, as the book often encourages, the outcomes can start to suffer. Code is not automatically better just because it reads nicely. Sometimes the more complex, repetitive, or unabstracted solution is the better one. Not because readability does not matter, but because it is only one constraint among many.&lt;/p&gt;
&lt;p&gt;To be clear: I care deeply about readable code. I try to write good, readable, and easily comprehensible code every day. But treating readability as the highest goal, rather than as one factor among many, leads to poor decisions.&lt;/p&gt;
&lt;h2 id=&quot;everything-is-a-tradeoff&quot;&gt;&lt;a href=&quot;#everything-is-a-tradeoff&quot;&gt;Everything Is a Tradeoff&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Readability trades off against other constraints. One of the most important is &lt;strong&gt;time&lt;/strong&gt;. Consider a function with 20 lines. You could split it into four sub-functions of 5 lines each to make it more readable. But doing so takes time. In most cases, that is a good investment: we read code more than we write it, so the upfront cost pays off later. But what if the code is just a verification script or a one-off codemod that will be deleted after running once? Then spending extra time on readability is a poor tradeoff.&lt;/p&gt;
&lt;p&gt;Readability also trades off against &lt;strong&gt;performance&lt;/strong&gt;. In React, &lt;code&gt;useCallback&lt;/code&gt; and &lt;code&gt;useMemo&lt;/code&gt; add noise and reduce readability. If readability were the highest goal, you would never use them. But they are sometimes necessary to make React perform well. Removing them for the sake of cleaner code would hurt the user experience.&lt;/p&gt;
&lt;p&gt;Or take &lt;strong&gt;DRY (Don’t Repeat Yourself)&lt;/strong&gt;, one of &lt;em&gt;Clean Code&lt;/em&gt;’s core principles. Follow it too strictly, and you abstract too early. You pull code into a shared function to avoid duplication, even though the two use cases will later diverge. Now your abstraction becomes technical debt. The right choice is not to eliminate all duplication, but to wait until the abstraction is clear and stable.&lt;/p&gt;
&lt;p&gt;The same is true of almost any coding principle. Immutable data can use more memory. Reusable code can be harder to change. Small functions can obscure the overall flow. No coding principle is universal. Whether it helps or hurts depends on the situation.&lt;/p&gt;
&lt;p&gt;And ultimately, your most important job is not writing readable code, it is &lt;strong&gt;creating business success&lt;/strong&gt;. Your manager cares about delivering value that can be sold. Your customers care about the problems you solve, not whether your code is properly abstracted and well named. Doing those things well takes significant time, time that might be better invested elsewhere to create business impact.&lt;/p&gt;
&lt;p&gt;This does not mean you should write sloppy code. Short-term business pressure versus long-term maintainability is itself a tradeoff. Rushing features out the door with poor code quality will slow you down later. But obsessing over perfect abstractions and pristine naming while missing deadlines and losing market opportunities is equally harmful.&lt;/p&gt;
&lt;h2 id=&quot;read-it-anyway&quot;&gt;&lt;a href=&quot;#read-it-anyway&quot;&gt;Read It Anyway&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So why is &lt;em&gt;Clean Code&lt;/em&gt; dangerous? Because it presents engineering tradeoffs as moral absolutes. The book does not just teach techniques, it judges you for not using them. If you do not follow its principles, you are doing bad, sloppy work. That framing turns situational decisions into a test of your professionalism, and it pushes engineers toward dogmatic thinking rather than contextual judgment.&lt;/p&gt;
&lt;p&gt;The techniques themselves are valuable. Small functions, clear naming, removing duplication, these are useful tools. But there is no such thing as universally “clean” code, only code that makes the right tradeoffs for its context.&lt;/p&gt;
&lt;p&gt;I still think &lt;em&gt;Clean Code&lt;/em&gt; is worth reading. Learn the techniques. Understand the reasoning. But treat the book as a collection of useful patterns, not a moral framework. Apply the ideas where they make sense. Ignore them where they do not.&lt;/p&gt;
&lt;p&gt;What makes someone a good engineer is not how strictly they follow one philosophy, but how well they understand the tradeoffs they are making.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;I’d genuinely be interested to hear your thoughts. Please drop your feedback on &lt;a href=&quot;https://app.daily.dev/posts/vdrncp0ik&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;daily.dev&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/posts/fabianletsch_clean-code-is-a-lie-activity-7445234126863073280-1ClX&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;LinkedIn&lt;/a&gt;! &amp;lt;- This is not marketing talk, I actually care!&lt;/p&gt;</content:encoded><wordCount>735</wordCount><updated>2026-05-10T22:03:10.000Z</updated><category>bestpractices</category><category>craftsmanship</category><author>Fabian Letsch</author><enclosure url="https://letsch.dev/_astro/clean-code-is-a-lie.CGJD-jJx.webp" length="4325376" type="image/webp"/></item><item><title>Delivering Quality Software at Breakneck Speed</title><link>https://letsch.dev/blog/delivering-quality-software-at-breakneck-speed/</link><guid isPermaLink="true">https://letsch.dev/blog/delivering-quality-software-at-breakneck-speed/</guid><description>Delivering high-quality software in fast-moving environments by using pragmatism, short feedback loops, ownership, and automation.</description><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;After working at three startups, I’ve learned that the real challenge is not moving fast. It is moving fast without creating chaos. When quality drops too far, speed stops being an advantage and starts turning into tech debt, bugs, frustrated customers, and lost revenue.&lt;/p&gt;
&lt;p&gt;Startups usually accept this trade-off. They “move fast and break things” and ship “80/20 solutions.” That is often the right business decision, especially in highly competitive markets, where you need the advantage of innovating first. But I believe teams can &lt;strong&gt;“move fast and break less”&lt;/strong&gt; if they are intentional about how they work.&lt;/p&gt;
&lt;p&gt;The problem is that many practices that improve quality also add friction. TDD, broad test coverage, and having multiple PR reviewers can all be valuable and can even save time in the long run. But in fast-moving environments, some of these investments do not pay off quickly enough. And if a team is already struggling to keep up with one reviewer, requiring two is not realistic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So the question becomes: how do you deliver quality software at breakneck speed?&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;1-short-feedback-loops&quot;&gt;&lt;a href=&quot;#1-short-feedback-loops&quot;&gt;1. Short Feedback Loops&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The earlier a problem is detected, the cheaper it is to fix.&lt;/p&gt;
&lt;p&gt;A bug caught while a developer is still typing is almost free. In code review, the cost is higher. In QA, higher again. And if a customer is the one who finds it, the cost is at its peak: more process kicks in, unnecessary communication follows, the developer has to rebuild their mental model of the code, and the customer loses trust.&lt;/p&gt;
&lt;p&gt;Thats why short feedback loops matter so much. They improve quality while actively removing overhead.&lt;/p&gt;
&lt;p&gt;For this reason I invest heavily in tooling like strong typing, &lt;a href=&quot;/blog/linters-the-most-underabused-tool-in-the-ai-slop-era&quot;&gt;linting, and static analysis&lt;/a&gt;. If a problem can be detected directly in the editor, it should be.&lt;/p&gt;
&lt;p&gt;For example, I once added a linter rule that detects when an icon is used as the only child of a button and suggests using the &lt;code&gt;IconButton&lt;/code&gt; component instead. That moves feedback from code review or design QA directly into the editor and prevents accessibility and design issues before they ship.&lt;/p&gt;
&lt;h2 id=&quot;2-pragmatism&quot;&gt;&lt;a href=&quot;#2-pragmatism&quot;&gt;2. Pragmatism&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To me, being pragmatic means solving the problem in the clearest and simplest way possible.&lt;/p&gt;
&lt;p&gt;Complexity kills both speed and quality. &lt;a href=&quot;/blog/being-smart-is-painful&quot;&gt;If code is unnecessarily hard to understand&lt;/a&gt;, changing it takes longer, and mistakes become more likely.&lt;/p&gt;
&lt;p&gt;A seasoned developer has many tools in their toolbox: complex type systems, microservices, abstractions, SOLID principles, design patterns, and more. These tools are not bad, but using them is expensive. They only pay off when they actually fit the problem and when the problem is stable enough. In a greenfield project, that often is not the case. Requirements may change again next week.&lt;/p&gt;
&lt;p&gt;The question you should always ask yourself is: &lt;em&gt;“What is the simplest solution that is good enough?”&lt;/em&gt; not &lt;em&gt;“What is the most elegant way to solve this?”&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;3-clear-ownership&quot;&gt;&lt;a href=&quot;#3-clear-ownership&quot;&gt;3. Clear ownership&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Many quality problems are caused by ambiguity.&lt;/p&gt;
&lt;p&gt;A typical example: someone posts in the group chat, “I found bug X.” Then what happens? Sometimes two people start looking into it. Sometimes nobody does. Sometimes everyone assumes someone else will handle it, and in the end it lands in production, moving the feedback loop even further to the right. That is wasted time, avoidable confusion, and lower product quality.&lt;/p&gt;
&lt;p&gt;Whether it is a bug, a feature, or just something somebody noticed, if it is worth mentioning, then somebody should be responsible for driving it to the next reliable state: building it, fixing it, assigning it, prioritizing it, or explicitly deciding not to act on it yet.&lt;/p&gt;
&lt;p&gt;This does not mean that whoever notices a problem must always solve it personally. It means they should not let it remain unowned. If ownership is handed over, that handoff should be explicit. If you cannot name the person you handed ownership to, then you did not do it properly.&lt;/p&gt;
&lt;p&gt;Clear ownership improves quality because issues are less likely to be forgotten, and it improves speed because it prevents duplicate work.&lt;/p&gt;
&lt;h2 id=&quot;4-automation&quot;&gt;&lt;a href=&quot;#4-automation&quot;&gt;4. Automation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Software drifts unless something pushes back. Standards fade, steps get skipped, and “we usually do it this way” slowly turns into inconsistency.&lt;/p&gt;
&lt;p&gt;That is why I do not put too much trust anymore in rules that live only in documentation, PR review reminders, or team habits. If a rule is not enforced, it is only a suggestion. And suggestions do not survive pressure.&lt;/p&gt;
&lt;p&gt;Automation is powerful because it does more than save time. It fights entropy. It turns standards from something people should remember into something the system consistently enforces.&lt;/p&gt;
&lt;p&gt;This is why I like &lt;a href=&quot;/blog/linters-the-most-underabused-tool-in-the-ai-slop-era&quot;&gt;linters&lt;/a&gt;, CI checks, and static analysis tools. They enforce standards reliably: dead code gets removed, &lt;code&gt;IconButton&lt;/code&gt; gets used for icon-only buttons, and referenced translation keys must exist.&lt;/p&gt;
&lt;p&gt;I also like AI code reviews as an additional layer in pull requests. They are less deterministic than linter rules or CI checks, but they can still catch suspicious patterns, missing context, and edge cases that humans might miss.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Quality and speed are not enemies, but they do require trade-offs. In fast-moving environments, you rarely get quality and speed from heavy engineering processes. &lt;strong&gt;You get them by making quality cheap to enforce.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For me, that comes down to four things: shortening feedback loops, being pragmatic, creating clear ownership, and automating what should not depend on memory or discipline.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;Do you know other ways to ensure quality while staying fast? Please let me know on &lt;a href=&quot;https://dly.to/1AZku6jrOzs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;daily.dev&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/posts/fabianletsch_delivering-quality-software-at-breakneck-activity-7441268098747469824-0Dr0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;</content:encoded><wordCount>910</wordCount><updated>2026-04-19T22:03:05.000Z</updated><category>bestpractices</category><category>craftsmanship</category><author>Fabian Letsch</author><enclosure url="https://letsch.dev/_astro/break-neck-speed.DJljM7Or.webp" length="1705600" type="image/webp"/></item><item><title>AI Made Me Hate My Job... Then I Found New Joy</title><link>https://letsch.dev/blog/ai-made-me-hate-my-job/</link><guid isPermaLink="true">https://letsch.dev/blog/ai-made-me-hate-my-job/</guid><description>AI made me resent coding, until I reframed my role. Here’s how I found new joy in outcomes, AI craftsmanship, and quality ownership.</description><pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A few weeks ago, we had a regular company meeting. Nothing dramatic, until my boss said: “We should use way more AI for software development. We need to speed up.”&lt;/p&gt;
&lt;p&gt;It makes sense. I get it.&lt;/p&gt;
&lt;p&gt;Still, I felt this heavy drop in my stomach. I was afraid that AI would replace the part of me that loved this job.&lt;/p&gt;
&lt;p&gt;Because I absolutely love to write the code myself! I love the struggles, thinking the solution through, finding all the places I have to consider, and then, at the end, feeling like Einstein himself when it’s finally working.&lt;/p&gt;
&lt;p&gt;Also, AI does so many stupid things: It forgets to update related files. It “helpfully” invents patterns inconsistent with the codebase. It tries to complete the task, but it doesn’t realize when the task itself doesn’t make sense. It writes plausible tests that assert the wrong behavior. The ways AI can mess up are infinite!&lt;/p&gt;
&lt;h2 id=&quot;people-dont-pay-for-code-they-pay-for-outcomes&quot;&gt;&lt;a href=&quot;#people-dont-pay-for-code-they-pay-for-outcomes&quot;&gt;People Don’t Pay for Code. They Pay for Outcomes.&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;And yet… on a good day, it saves me hours, and honestly, people do not pay me for being someone who codes or for having fun at work. The customer pays me for solving their needs. Management pays me for building a product that sells.&lt;/p&gt;
&lt;p&gt;I started to notice a subtle resentment building up. I could see the direction clearly: less time for careful thinking, less pride in the craft, more pressure to “just ship,” and my role drifting from builder to operator.&lt;/p&gt;
&lt;p&gt;For the first time in years, I caught myself thinking: If this is the future of my work, will I still want to do it?&lt;/p&gt;
&lt;h2 id=&quot;onto-a-journey-to-find-joy&quot;&gt;&lt;a href=&quot;#onto-a-journey-to-find-joy&quot;&gt;Onto a journey to find Joy&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I was recently on vacation and had time to step back and think. I realized that if AI is going to handle most of the actual coding, then there must be other things that can be fun, I mean my colleagues who do not code at all seem to enjoy their work. So I decided to look for the things that make me enjoy my work besides coding, and I found plenty!&lt;/p&gt;
&lt;h3 id=&quot;joy-1-solving-real-problems-for-real-people&quot;&gt;&lt;a href=&quot;#joy-1-solving-real-problems-for-real-people&quot;&gt;Joy #1: Solving Real Problems for Real People&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Taking ownership in not just the code, but the product you are building puts you right back at the driver’s seat. Helping someone, clarifying what they truly need, and shipping something that makes their day easier still feels deeply satisfying. And it’s meaningful to challenge assumptions, because it actually makes a difference to build the right thing, even if you are not building it by hand.&lt;/p&gt;
&lt;h3 id=&quot;joy-2-ai-craftsmanship&quot;&gt;&lt;a href=&quot;#joy-2-ai-craftsmanship&quot;&gt;Joy #2: AI Craftsmanship&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I always thought that using AI means my own acquired knowledge and craftsmanship just was replaced, but in actuality it also is real craftsmanship to use AI well. Instead of trying to be better at my original craft, I can instead try to become better at producing with AI. I can gain knowledge and experience in setting constraints, feeding it the right context and giving it the right tools. If AI is going to stay, then I better become one of the best at using it.&lt;/p&gt;
&lt;h3 id=&quot;joy-3-ensuring-quality&quot;&gt;&lt;a href=&quot;#joy-3-ensuring-quality&quot;&gt;Joy #3: Ensuring Quality&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;AI can write a lot of wrong code really fast. It can make wrong architecture decisions or write something in a thousand lines when it shouldn’t be more than a hundred. That’s why I’ve started finding joy in quality ownership. One way to do that is going through a proper research and planning phase with the AI, asking it for solution proposals and then making an implementation plan together. Another is to do proper code reviews, &lt;a href=&quot;/blog/delivering-quality-software-at-breakneck-speed&quot;&gt;catching issues early&lt;/a&gt; before the customer has to. As a German, I’ve always resonated with the craftsmanship mindset: precision, reliability, and doing things right even when nobody is watching.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;AI increased my throughput, but it also changed what feels meaningful and enjoyable about my work. I’m learning to shift my identity from “writer of code” to “builder of product outcomes” and this can also be a lot of fun.&lt;/p&gt;
&lt;p&gt;The joy didn’t disappear. It changed its location.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;Have you gone through the same process? Are you excited or scared about the future of software development? I would love to hear about it on &lt;a href=&quot;https://app.daily.dev/posts/vqwneq1gd&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;daily.dev&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/posts/fabianletsch_ai-made-me-hate-my-job-then-i-found-new-activity-7436180437829505025-76_3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;</content:encoded><wordCount>703</wordCount><updated>2026-04-19T22:03:05.000Z</updated><category>craftsmanship</category><category>mindset</category><author>Fabian Letsch</author><enclosure url="https://letsch.dev/_astro/ai-made-me-hate-my-job.DzNGln5S.webp" length="4325376" type="image/webp"/></item><item><title>Beating Distractions: A Survival Guide</title><link>https://letsch.dev/blog/beating-distractions-a-survival-guide/</link><guid isPermaLink="true">https://letsch.dev/blog/beating-distractions-a-survival-guide/</guid><description>Constantly switching between Reddit, YouTube and news? Use these tools and tactics to quit doomscrolling and focus.</description><pubDate>Sun, 12 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Lets be real, its way too easy to spend your “work” day jumping between social media, news, daily.dev, YouTube and reddit. If you’re anything like me, it’s tempting to tell yourself that learning new things is the same as actually getting work done.&lt;/p&gt;
&lt;p&gt;I have spent many years trying to beat this, but on my own i never really succeeded. Then i found the following tools and since then, its way easier:&lt;/p&gt;
&lt;h2 id=&quot;cold-turkey-blocker&quot;&gt;&lt;a href=&quot;#cold-turkey-blocker&quot;&gt;Cold Turkey Blocker&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Cold Turkey Blocker blocks sites across all browsers and blocks desktop apps and games as well. It lets you set timers during which you cant unblock it. The best part? Its smarter than you are: change your system time? Nope. Kill the process in Task Manager? Good luck. Its stubborn, and thats exactly what I needed, and you might too. I HIGHLY recommend it!&lt;/p&gt;
&lt;h2 id=&quot;onesec-app&quot;&gt;&lt;a href=&quot;#onesec-app&quot;&gt;onesec App&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For your phone, onesec is a genius little app. Whenever you try to open a distracting app, it throws up a long, unskippable screen. You have to wait a few seconds before you can proceed, which is just enough time to realize that you dont actually want to go there.&lt;/p&gt;
&lt;h2 id=&quot;phone-lock-box&quot;&gt;&lt;a href=&quot;#phone-lock-box&quot;&gt;Phone Lock Box&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The easy solution would just be to put your phone in the next room, but if that doesnt work for you, this one might help: put your muted phone in a lockbox and set a timer – No notifications, no doomscrolling, just you and your work. 🫠&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;If you’re battling the same distractions, start with one tool and one habit, and give it a week. Small changes add up fast and future you will thank you.&lt;/p&gt;
&lt;p&gt;What are your secret weapons against distractions? Please let me know on &lt;a href=&quot;https://app.daily.dev/posts/beating-distractions-a-survival-guide-exbdyoryx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;daily.dev&lt;/a&gt;.&lt;/p&gt;</content:encoded><wordCount>288</wordCount><updated>2026-04-19T21:43:21.000Z</updated><category>productivity</category><author>Fabian Letsch</author><enclosure url="https://letsch.dev/_astro/beating-distractions-a-survival-guide.BMA-L8aJ.webp" length="1705600" type="image/webp"/></item><item><title>Do Your Comments Help or Hurt?</title><link>https://letsch.dev/blog/do-your-comments-help-or-hurt/</link><guid isPermaLink="true">https://letsch.dev/blog/do-your-comments-help-or-hurt/</guid><description>Write cleaner code with fewer, better comments. Avoid TODO clutter and learn what&apos;s worth documenting and why.</description><pubDate>Thu, 02 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Have you ever seen a stop sign with another sign below it that says, “This is a stop sign”?&lt;/p&gt;
&lt;p&gt;…&lt;/p&gt;
&lt;p&gt;No, me neither.&lt;/p&gt;
&lt;p&gt;Yet, step into any codebase and you’ll trip over comments doing exactly that:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/_astro/do-your-comments-help-or-hurt-2.DgTHvvVV_2uMpu8.webp&quot; alt=&quot;A redudant comment shown above the code&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;800&quot; height=&quot;353&quot;&gt;&lt;/p&gt;
&lt;p&gt;The Department of Redundancy Department strikes again!&lt;/p&gt;
&lt;p&gt;Now, maybe you’re thinking, “Hey, it’s just some harmless noise.” Sure, but don’t get too comfortable, because i have a whole bunch more!&lt;/p&gt;
&lt;h3 id=&quot;1-lies-damned-lies-and-comments&quot;&gt;&lt;a href=&quot;#1-lies-damned-lies-and-comments&quot;&gt;1. Lies, damned lies and Comments&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Comments sometimes are like politicians: they promise one thing and deliver another. Maybe someone updated the function but forgot about the comment, or they just copy-pasted without updating it.&lt;/p&gt;
&lt;p&gt;No matter what is the cause: The comments might lie, the code will not.&lt;/p&gt;
&lt;h3 id=&quot;2-todo-or-not-todo-that-is-the-question&quot;&gt;&lt;a href=&quot;#2-todo-or-not-todo-that-is-the-question&quot;&gt;2. TODO or NOT TODO, that is the question&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Do you know the situation where you leave something in the back of the fridge and months later it’s still there, completely spoiled, and you pretend not to see it? &lt;code class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// TODO: Refactor this.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt; is the digital equivalent of exactly that.&lt;/p&gt;
&lt;p&gt;Adding those TODOs while developing is fine; But merging a pull request littered with TODOs? &lt;em&gt;Nah.&lt;/em&gt; Do your team a favor: turn TODOs into real tasks in your project management tool and actually assign them. But beware because in task management land a place called “Backlog” exists, which is a synonym for “Graveyard”.&lt;/p&gt;
&lt;p&gt;Speaking of graveyards…&lt;/p&gt;
&lt;h3 id=&quot;3-comments-are-not-a-code-graveyard&quot;&gt;&lt;a href=&quot;#3-comments-are-not-a-code-graveyard&quot;&gt;3. Comments Are Not a Code Graveyard&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Commented-out code just clutters the codebase. If you’re not using code anymore, delete it. You can always find old versions in the Git history if you need them. In nine years as a developer, I’ve rarely had to recover old code from comments.&lt;/p&gt;
&lt;p&gt;So then what does a &lt;em&gt;useful&lt;/em&gt; comment look like?&lt;/p&gt;
&lt;hr/&gt;
&lt;h2 id=&quot;practical-tips-for-writing-good-comments&quot;&gt;&lt;a href=&quot;#practical-tips-for-writing-good-comments&quot;&gt;Practical Tips for Writing Good Comments&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;1-do-not-write-the-comment-in-the-first-place&quot;&gt;&lt;a href=&quot;#1-do-not-write-the-comment-in-the-first-place&quot;&gt;1. Do not write the comment in the first place&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Whenever you think you will need a comment to explain what is happening, pause. &lt;em&gt;Can you clarify the code?&lt;/em&gt; Comments become unnecessary when the code itself tells the story.&lt;/p&gt;
&lt;h3 id=&quot;2-explain-the-why-not-the-what&quot;&gt;&lt;a href=&quot;#2-explain-the-why-not-the-what&quot;&gt;2. Explain the &lt;em&gt;Why&lt;/em&gt;, Not the &lt;em&gt;What&lt;/em&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The computer cares about &lt;em&gt;what&lt;/em&gt; the code does. Your fellow developers care about &lt;em&gt;why&lt;/em&gt; you made it do that weird thing. If you had to pick a workaround for a browser bug, or hack something to work around an API quirk, write &lt;em&gt;that&lt;/em&gt; down.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do not do this:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes catppuccin-mocha catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;--shiki-dark-bg:#1e1e2e;color:#cdd6f4;--shiki-dark:#cdd6f4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;--shiki-light-font-style:italic;--shiki-dark:#9399B2;--shiki-dark-font-style:italic&quot;&gt;// Wrapper div is needed&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Do this instead:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes catppuccin-mocha catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;--shiki-dark-bg:#1e1e2e;color:#cdd6f4;--shiki-dark:#cdd6f4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;--shiki-light-font-style:italic;--shiki-dark:#9399B2;--shiki-dark-font-style:italic&quot;&gt;// Wrapper div is required for Firefox, see: https://bugzilla...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-reference-issues-and-tickets-directly&quot;&gt;&lt;a href=&quot;#3-reference-issues-and-tickets-directly&quot;&gt;3. Reference Issues and Tickets directly&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Link to relevant issues, Jira tickets, or merge requests right in your comment. This gives context &lt;em&gt;and&lt;/em&gt; traceability.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes catppuccin-mocha catppuccin-mocha&quot; style=&quot;background-color:#1e1e2e;--shiki-dark-bg:#1e1e2e;color:#cdd6f4;--shiki-dark:#cdd6f4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;--shiki-light-font-style:italic;--shiki-dark:#9399B2;--shiki-dark-font-style:italic&quot;&gt;// Some users don&amp;#39;t have an id... Why? See issue: #756&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CBA6F7;--shiki-dark:#CBA6F7&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#94E2D5;--shiki-dark:#94E2D5&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#CDD6F4;--shiki-dark:#CDD6F4&quot;&gt;id) &lt;/span&gt;&lt;span style=&quot;color:#9399B2;--shiki-dark:#9399B2&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;--shiki-light-font-style:italic;--shiki-dark:#9399B2;--shiki-dark-font-style:italic&quot;&gt;    // handle missing id&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9399B2;--shiki-dark:#9399B2&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That way, when someone’s looking through the code six months later, they will not end up deleting something that is actually needed or asking you why its there.&lt;/p&gt;
&lt;h3 id=&quot;4-keep-it-simple-stupid&quot;&gt;&lt;a href=&quot;#4-keep-it-simple-stupid&quot;&gt;4. KEEP IT SIMPLE, STUPID&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Nobody is going to read a novel in a code comment. If you need more than a couple of lines, consider if the information should go elsewhere, such as developer documentation or a linked issue.&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;Write &lt;a href=&quot;/blog/being-smart-is-painful&quot;&gt;simple code&lt;/a&gt; that speaks for itself, comment only to explain the &lt;em&gt;why&lt;/em&gt; and don’t litter the code with TODOs nobody will tackle.&lt;/p&gt;
&lt;p&gt;Is there more to say about comments? Please tell me on &lt;a href=&quot;https://app.daily.dev/posts/do-your-comments-help-or-hurt--10p66dkop&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;daily.dev&lt;/a&gt;.&lt;/p&gt;</content:encoded><wordCount>518</wordCount><updated>2026-04-19T21:43:21.000Z</updated><category>bestpractices</category><category>craftsmanship</category><author>Fabian Letsch</author><enclosure url="https://letsch.dev/_astro/do-your-comments-help-or-hurt.NUAU4XDZ.webp" length="4325376" type="image/webp"/></item><item><title>Being &quot;Smart&quot; Is Painful</title><link>https://letsch.dev/blog/being-smart-is-painful/</link><guid isPermaLink="true">https://letsch.dev/blog/being-smart-is-painful/</guid><description>“Smart” patterns often hide complexity. Learn why clever abstractions hurt teams and how boring code stays maintainable.</description><pubDate>Thu, 25 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;look-at-this-marvel&quot;&gt;&lt;a href=&quot;#look-at-this-marvel&quot;&gt;Look At This Marvel:&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/_astro/being-smart-is-painful-2.D75kIgFo_Z4kRLy.webp&quot; alt=&quot;Code snippet showing a confusing TypeScript namespace pattern&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;800&quot; height=&quot;338&quot;&gt;&lt;/p&gt;
&lt;p&gt;With this you will never have to &lt;code class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { ButtonProps } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;./Button&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt; ever again! Just &lt;code class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { Button } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;./Button&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt; and voila! Component &lt;code class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;Button &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt; and Props &lt;code class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;Button.Props&lt;/span&gt;&lt;/span&gt;&lt;/code&gt; are right there.&lt;/p&gt;
&lt;p&gt;Isn’t it smart?&lt;/p&gt;
&lt;p&gt;…&lt;/p&gt;
&lt;p&gt;Well yes it is and that is exactly the problem!&lt;/p&gt;
&lt;p&gt;You see, in software development &lt;strong&gt;SMART&lt;/strong&gt; usually means &lt;strong&gt;COMPLICATED&lt;/strong&gt; and if i have learned anything over the years it is that &lt;strong&gt;COMPLICATED&lt;/strong&gt; means &lt;strong&gt;BAD&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And this code is smart and complicated for so many reasons!!&lt;/p&gt;
&lt;h2 id=&quot;1-you-hardly-ever-import-props-directly&quot;&gt;&lt;a href=&quot;#1-you-hardly-ever-import-props-directly&quot;&gt;1. You Hardly Ever Import Props Directly&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Seriously, how often do you actually need to import the props from outside the component file? Maybe twice a year? Forcing every file to bend to this “smart” import logic is like carrying an umbrella every day because it once rained in 2017.&lt;/p&gt;
&lt;h2 id=&quot;2-every-developer-and-their-mum-will-ask-why&quot;&gt;&lt;a href=&quot;#2-every-developer-and-their-mum-will-ask-why&quot;&gt;2. Every Developer And Their Mum Will Ask “Why?”&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Yeah, it’s clever. For about 5 minutes. Then you realize nobody will remember two months from now why the Button has a mysterious namespace. “Props” isn’t just a type anymore, it’s an easter egg with a riddle.&lt;/p&gt;
&lt;h2 id=&quot;3-weird-edge-cases&quot;&gt;&lt;a href=&quot;#3-weird-edge-cases&quot;&gt;3. Weird Edge Cases&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Have you tried assigning namespaces to an object?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/_astro/being-smart-is-painful-3.A4yFGCJ8_Z2fVRnj.webp&quot; alt=&quot;Code snippet: const components =  Button &quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;800&quot; height=&quot;306&quot;&gt;&lt;/p&gt;
&lt;p&gt;Well lets keep this short, it doesn’t work. So when you ever run into this, you will have to solve this in an even more complicated way… Congrats!&lt;/p&gt;
&lt;h2 id=&quot;4-even-llms-dont-get-it&quot;&gt;&lt;a href=&quot;#4-even-llms-dont-get-it&quot;&gt;4. Even LLMs don’t get it&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As this code is unusual LLMs will not get it and will definitely not start writing it themselves unless you add a rule for it. Just like devs, LLMs work better with what’s idiomatic. (Idiomatic means: Code that follows the patterns that people are used to.)&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;&lt;strong&gt;As Grug once said “complexity very, very bad”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The truth is, the best devs don’t add complexity, they remove it. Being able to write smart code, but then not doing it. That is true wisdom. With smart code you will get: More bugs, more questions, more dependence, more confusion, more time spent reading code and more time spent explaining your idiocy to LLMs…&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Let my pain be your warning!&lt;/strong&gt; Do yourself a favor and write your Props the boring way. In fact, write all your code the boring way. Your future self will thank you!&lt;/p&gt;
&lt;hr/&gt;
&lt;p&gt;Have you been “Smart” before? I’d genuinely love to hear about it on &lt;a href=&quot;https://dly.to/eaGxMrIi5iQ&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; class=&quot;text-link&quot;&gt;daily.dev&lt;/a&gt;.&lt;/p&gt;</content:encoded><wordCount>380</wordCount><updated>2026-05-04T22:08:10.000Z</updated><category>bestpractices</category><category>craftsmanship</category><author>Fabian Letsch</author><enclosure url="https://letsch.dev/_astro/being-smart-is-painful.By41NX68.webp" length="4325376" type="image/webp"/></item></channel></rss>