<?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[Better Frontend]]></title><description><![CDATA[For developers who've debugged enough production fires to know that frontend architecture, security, and UX actually deserve serious thought.]]></description><link>https://www.better-frontend.dev</link><image><url>https://substackcdn.com/image/fetch/$s_!y6Jq!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fab11d57e-44b1-4c97-8fae-6e096e2a8178_512x512.png</url><title>Better Frontend</title><link>https://www.better-frontend.dev</link></image><generator>Substack</generator><lastBuildDate>Wed, 13 May 2026 11:06:50 GMT</lastBuildDate><atom:link href="https://www.better-frontend.dev/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Ireneusz Budzowski]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[irek.budzowski@gmail.com]]></webMaster><itunes:owner><itunes:email><![CDATA[irek.budzowski@gmail.com]]></itunes:email><itunes:name><![CDATA[Ireneusz Budzowski]]></itunes:name></itunes:owner><itunes:author><![CDATA[Ireneusz Budzowski]]></itunes:author><googleplay:owner><![CDATA[irek.budzowski@gmail.com]]></googleplay:owner><googleplay:email><![CDATA[irek.budzowski@gmail.com]]></googleplay:email><googleplay:author><![CDATA[Ireneusz Budzowski]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Hiring in the Age of AI: How to Stop Interviewing a Chatbot by Accident]]></title><description><![CDATA[Or: I spent an hour today asking questions that ChatGPT answers better than the candidate, and somehow I was the one who felt embarrassed]]></description><link>https://www.better-frontend.dev/p/hiring-in-the-age-of-ai-how-to-stop</link><guid isPermaLink="false">https://www.better-frontend.dev/p/hiring-in-the-age-of-ai-how-to-stop</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Mon, 13 Apr 2026 15:46:45 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!DA8N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff815526-1827-4302-93c2-ef746fde43a9_1024x608.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I did an interview today. I walked out of it with two things: a page of notes and the specific flavour of headache you only get from an hour of polite interrogation that neither party particularly enjoyed. Because frontend hiring in 2026 has evolved into a beautifully absurd ritual where two grown adults sit on a video call and carefully pretend the third person in the room does not exist.</p><p>You know the one. The one with the blinking cursor in the other tab. The one typing faster than both of us combined.</p><p>The candidate pretends they are not using it. I pretend I believe them. We both pretend this is a meaningful signal about whether they can ship a feature without setting the build pipeline on fire. Everybody leaves the call feeling vaguely unclean. Ten out of ten, no notes, would not recommend to a friend.</p><p>Except I do have notes. A lot of them. I also have opinions, because I have been stewing on this all afternoon and the coffee has not helped. So here we are. This post is for two people: the person who has to run the interview and does not know where to point the flashlight anymore, and the person on the other end of that call who read three LinkedIn threads about &#8220;how to beat AI-powered interviews&#8221; and somehow came out more confused than they went in. Both of you are going to get roasted, gently. It is for your own good.</p><h2>The CV that ChatGPT wrote and nobody proofread</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!DA8N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff815526-1827-4302-93c2-ef746fde43a9_1024x608.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!DA8N!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff815526-1827-4302-93c2-ef746fde43a9_1024x608.png 424w, https://substackcdn.com/image/fetch/$s_!DA8N!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff815526-1827-4302-93c2-ef746fde43a9_1024x608.png 848w, https://substackcdn.com/image/fetch/$s_!DA8N!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff815526-1827-4302-93c2-ef746fde43a9_1024x608.png 1272w, https://substackcdn.com/image/fetch/$s_!DA8N!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff815526-1827-4302-93c2-ef746fde43a9_1024x608.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!DA8N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff815526-1827-4302-93c2-ef746fde43a9_1024x608.png" width="1024" height="608" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ff815526-1827-4302-93c2-ef746fde43a9_1024x608.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:608,&quot;width&quot;:1024,&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_!DA8N!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff815526-1827-4302-93c2-ef746fde43a9_1024x608.png 424w, https://substackcdn.com/image/fetch/$s_!DA8N!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff815526-1827-4302-93c2-ef746fde43a9_1024x608.png 848w, https://substackcdn.com/image/fetch/$s_!DA8N!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff815526-1827-4302-93c2-ef746fde43a9_1024x608.png 1272w, https://substackcdn.com/image/fetch/$s_!DA8N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff815526-1827-4302-93c2-ef746fde43a9_1024x608.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><figcaption class="image-caption">Robot on a video call </figcaption></figure></div><p>Let us start before the call even happens. Let us start with the CV.</p><p>For about a year now I have been seeing documents that look less like CVs and more like character sheets from a game I never agreed to play. &#8220;React 85%. Angular 90%. TypeScript 80%. Node 75%.&#8221; Clean, round, symmetrical, suspiciously confident, all suspiciously ending in five or zero as if the candidate&#8217;s skills were rounded for cashier convenience. It is the kind of precision nobody in the history of software has ever felt about their own abilities. I have been doing frontend for nearly two decades and I still could not tell you whether I am &#8220;87% Angular&#8221; or &#8220;63% Angular&#8221; or just &#8220;a guy who mostly remembers where the semicolons go and when to stop arguing about tabs.&#8221;</p><p><strong>Nobody writes about themselves in percentages.</strong> It is not how humans self-assess. It is, however, exactly how an LLM fills in a table when you politely ask it to &#8220;make my CV look more professional.&#8221; And the LLM, bless it, does exactly what you asked, with the quiet competence of a kid who really wants that gold star.</p><p>And I am not alone in noticing. The numbers on this are actually pretty stark: around two-thirds of job candidates now use AI somewhere in their application process, 90% of hiring managers report a spike in low-effort spammy applications, and 78% of companies actively check for AI-generated content. One Resume Now survey of 925 HR workers found that 62% of hiring managers say AI-generated resumes without personalization lead to rejections. So when I tell you &#8220;I can smell an AI-written CV from orbit,&#8221; I am not being dramatic, I am describing the average Tuesday for anyone doing frontend hiring in 2026.</p><p>To be completely clear: I am a huge fan of numbers on a CV. I love a good SMART bullet point. &#8220;Cut cold start from 2.1s to 380ms on the pricing page.&#8221; &#8220;Migrated 140 components from AngularJS to Angular 17 over eight months.&#8221; &#8220;Replaced our home-grown form library with Reactive Forms and removed 4,000 lines of dead code.&#8221; These are gorgeous. I want to hear the story behind every single one. Give me more of those.</p><p>What breaks the spell is not the presence of numbers. It is the <strong>too-good-to-be-true energy</strong> of the whole document. You know the vibe:</p><blockquote><p>&#8220;Increased application performance by 47%.&#8221;</p><p>&#8220;Improved conversion rate by 23%.&#8221;</p><p>&#8220;Contributed to revenue growth of 3.5M.&#8221;</p><p>&#8220;Reduced bundle size by 61% and improved user retention by 18%.&#8221;</p></blockquote><p>Four bullets. Every one a banger. No context, no baseline, no attribution, no &#8220;we tried three things and this was the one that worked.&#8221; Just a highlight reel of round numbers that all happen to land on the good side of zero. It reads less like a career and more like a pitch deck that an LLM generated after being asked to &#8220;make it sound impactful.&#8221;</p><p>This is not a trap question. It is honestly not even the candidate&#8217;s fault half the time, because the resume-builder tool literally prompted them with &#8220;add measurable impact here.&#8221; But the second you ask &#8220;walk me through the 18% retention number, what was the baseline, what was the measurement window, what else changed in that sprint, who owned that metric&#8221;, one of two things happens. Either a real story unfolds (and you relax, because the candidate was just over-polishing something real) or the whole thing deflates in about fifteen seconds. Both outcomes are useful. That is the point.</p><p>The tell, again, is not the numbers themselves. It is the pattern. When <strong>every single bullet</strong> sparkles and <strong>not one of them</strong> admits a trade-off, a failure, a &#8220;it worked but we spent three weeks on the wrong thing first&#8221;, what you are reading is not a career summary. It is a rendered document, and the person who rendered it may not even remember what went in. That is the thing to probe.</p><p><strong>What I actually look for in a CV now:</strong></p><ul><li><p>Projects described as projects, not as a vague cloud of &#8220;modern frameworks and best practices&#8221;</p></li><li><p>Technologies attached to actual work, not floating in a separate Skills section like product labels in a supermarket</p></li><li><p>Numbers that mean something (&#8221;cut initial render from 2.1s to 380ms&#8221;, &#8220;migrated 140 components from AngularJS to Angular 17 over eight months&#8221;) instead of numbers that mean nothing (&#8221;JavaScript 92%, soft skills 88%&#8221;)</p></li><li><p>The ability to expand every single bullet into a five-minute story. Because if you cannot, it is not your CV. It is a CV-shaped object that arrived in your inbox.</p></li></ul><p>That last one is the only real rule. The CV is not a document, it is a contract for the first 30 minutes of our conversation. Everything on it is fair game. If a line is too dangerous to defend, take it out before you hit send. Radical concept, I know.</p><h2>&#8220;Is the candidate cheating?&#8221; Wrong question.</h2><p>Now the elephant. Remote interviews. Second monitor. Cursor open in the background. Claude whispering sweet nothings into a hidden Airpod. The whole pantomime.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1760156885430-cd0bd9609ff6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxlbGVwaGFudCUyMGluJTIwdGhlJTIwcG9yY2VsYWlufGVufDB8fHx8MTc3NjA5NTU0OXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1760156885430-cd0bd9609ff6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxlbGVwaGFudCUyMGluJTIwdGhlJTIwcG9yY2VsYWlufGVufDB8fHx8MTc3NjA5NTU0OXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1760156885430-cd0bd9609ff6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxlbGVwaGFudCUyMGluJTIwdGhlJTIwcG9yY2VsYWlufGVufDB8fHx8MTc3NjA5NTU0OXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1760156885430-cd0bd9609ff6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxlbGVwaGFudCUyMGluJTIwdGhlJTIwcG9yY2VsYWlufGVufDB8fHx8MTc3NjA5NTU0OXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1760156885430-cd0bd9609ff6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxlbGVwaGFudCUyMGluJTIwdGhlJTIwcG9yY2VsYWlufGVufDB8fHx8MTc3NjA5NTU0OXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1760156885430-cd0bd9609ff6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxlbGVwaGFudCUyMGluJTIwdGhlJTIwcG9yY2VsYWlufGVufDB8fHx8MTc3NjA5NTU0OXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="4240" height="2832" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1760156885430-cd0bd9609ff6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxlbGVwaGFudCUyMGluJTIwdGhlJTIwcG9yY2VsYWlufGVufDB8fHx8MTc3NjA5NTU0OXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2832,&quot;width&quot;:4240,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Collection of small ceramic elephant figurines on a decorative plate.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&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="Collection of small ceramic elephant figurines on a decorative plate." title="Collection of small ceramic elephant figurines on a decorative plate." srcset="https://images.unsplash.com/photo-1760156885430-cd0bd9609ff6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxlbGVwaGFudCUyMGluJTIwdGhlJTIwcG9yY2VsYWlufGVufDB8fHx8MTc3NjA5NTU0OXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1760156885430-cd0bd9609ff6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxlbGVwaGFudCUyMGluJTIwdGhlJTIwcG9yY2VsYWlufGVufDB8fHx8MTc3NjA5NTU0OXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1760156885430-cd0bd9609ff6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxlbGVwaGFudCUyMGluJTIwdGhlJTIwcG9yY2VsYWlufGVufDB8fHx8MTc3NjA5NTU0OXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1760156885430-cd0bd9609ff6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxlbGVwaGFudCUyMGluJTIwdGhlJTIwcG9yY2VsYWlufGVufDB8fHx8MTc3NjA5NTU0OXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@kushalib15">Kushali Bhagat</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>Here is my confession. I used to care about this a lot. Like, embarrassingly a lot. The instinct is to build traps. Stricter challenges, trickier questions, increasingly baroque live-coding exercises, the whole interrogation kit. At my lowest point I was basically one step away from asking candidates to solve a LeetCode hard while balancing a glass of water on their head, just to prove they were really there. It does not work. It is miserable for everyone including me, good cheaters are still good at cheating, bad cheaters fail anyway, and at some point you catch your own reflection in the webcam during a 45-minute technical screen and realise you have become a TSA agent with a subscription to Pluralsight. Not a great look.</p><p>So I dropped it. Not because I stopped caring, but because I was asking the wrong question. The real question is not &#8220;how do I stop the candidate from using AI?&#8221; It is:</p><p><strong>Why am I asking anything that AI answers better than a human in the first place?</strong></p><p>Because if your interview question can be solved by a well-prompted LLM in three seconds, congratulations, you have just verified that your candidate knows how to type into a text box. That is a skill. It is just not the skill you are hiring for. Probably.</p><p>&#8220;What is a closure?&#8221; AI wins. &#8220;Explain useEffect.&#8221; AI wins. &#8220;Difference between let, const, and var?&#8221; AI wins, and also I am going to bed.</p><p>These were great questions in 2015 when Google was the cheat code and we all pretended it was not. They are now the equivalent of asking a driver to recite the dictionary definition of &#8220;steering wheel.&#8221;</p><p>So I moved the whole interview somewhere the model cannot follow.</p><h2>Interview the human, not the stack</h2><p>The questions that work now (and work precisely because they are AI-proof, not by accident but by design) go something like this:</p><ul><li><p>&#8220;Tell me about the worst bug you shipped in the last year. What was it, how did you find it, and what did you change so it would not happen again?&#8221;</p></li><li><p>&#8220;Describe a moment when you knew your team was making a bad architectural call, and you let it happen anyway. Why? What would you do differently today?&#8221;</p></li><li><p>&#8220;Tell me about a technical decision you are proud of. Now tell me about one you regret.&#8221;</p></li><li><p>&#8220;What is the hardest code review you have ever given or received? Not technically hard, emotionally hard.&#8221;</p></li><li><p>&#8220;When did you last change your mind about a pattern or a practice you used to defend? What convinced you?&#8221;</p></li></ul><p>None of these can be prompted. Not because they are clever, but because the answer requires specific humans, specific code, specific hallway conversations, specific 2 a.m. Slack messages. You can try to bluff, but you cannot get past the third follow-up question. There is always a third follow-up question. That is the trick. It is not a magic trick. It is just &#8220;keep asking &#8216;and then what&#8217;&#8221; until either a real memory surfaces or the whole thing collapses.</p><p>And here is the counterintuitive bit. The candidates I trust most are the ones who <strong>get things wrong in a very specific, human way</strong>. The ones who say &#8220;actually, I thought X for years and then I got bitten by it.&#8221; The ones who hesitate, reconsider, correct themselves. That hesitation is the sound of someone who has actually been in the room. Candidates who answer everything smoothly and confidently usually fall into one of two buckets: they have an earpiece, or they have never shipped anything that hurt them. I do not know which is worse.</p><p>Actually I do. The second one is worse. The first one at least owns a microphone.</p><h2>Trade-offs, opinions, and the dying art of having a take</h2><p>I still ask about patterns. I just do not ask what they are. I ask when you would not use them.</p><ul><li><p>&#8220;When are Angular signals actually better than RxJS, and when are they worse? Give me a concrete example of each.&#8221;</p></li><li><p>&#8220;Page-Feature Composition vs. classic smart/dumb components: where does PFC give you leverage, and where does it fall apart?&#8221;</p></li><li><p>&#8220;OnPush, trackBy, defer, lazy loading. Which of these actually move TTI, and which are just code hygiene pretending to be performance?&#8221;</p></li><li><p>&#8220;Last project where you consciously chose NOT to use a state management library even though there was state. Walk me through the reasoning.&#8221;</p></li></ul><p>I am not looking for correct answers. I am looking for an opinion that the candidate can defend without outsourcing. They can disagree with me. I want them to disagree with me. What I cannot work with is the verbal equivalent of a shrug wrapped in jargon. &#8220;It depends&#8221; is not an opinion, it is a survival strategy, and I am running out of patience for it.</p><p>The developers I want on my team have taste. Taste is the one thing the model cannot give them. It also happens to be the first thing that atrophies when you let the model drive every decision.</p><h2>The AI literacy test: not how you use it, why you think it works</h2><p>This is where it gets fun. I ask every candidate about AI now. Not to catch them, but to find out how deeply they actually understand the thing they are putting in their workflow.</p><p>Because I see two failure modes, and honestly both of them keep me up at night in different ways.</p><p><strong>Type 1: The Dabbler.</strong> Uses ChatGPT &#8220;sometimes, for small things.&#8221; Does not know which model is behind the chat. Has not heard of context windows. Genuinely thinks MCP stands for something Marvel-adjacent. For the Dabbler, AI is &#8220;a slightly faster Stack Overflow&#8221; and any attempt to discuss agents, tool use, or RAG produces the facial expression of someone being handed a wine list in a language they do not speak. A year from now the Dabbler will be lapped by anyone who took this seriously, and they will not even notice until a junior colleague reviews their PR and politely asks why they are still writing boilerplate by hand like it is 2022.</p><p><strong>Type 2: The Full Vibe Coder.</strong> Oh buddy. If you know, you know, and if you do not, I already wrote a whole post about this one called <a href="https://www.better-frontend.dev/p/stop-vibing-start-understanding-why">Stop Vibing, Start Understanding</a>, where I got most of the yelling out of my system, so I will try to be brief here. The Vibe Coder prompts &#8220;build me a login component&#8221; and accepts whatever falls out of the machine like a gumball from a supermarket dispenser. They are fast. Terrifyingly fast. Right up until something breaks, at which point they stare at their own codebase with the haunted expression of a person trying to read Linear A after a long weekend, because they did not actually write the code, they <em>commissioned</em> it, and you cannot debug a commission. They are a load-bearing incident waiting to happen. The incident will happen on a Friday. It is always a Friday. I do not make the rules.</p><p>Both of these people will tell you, with total sincerity and zero self-awareness, that they &#8220;use AI a lot.&#8221; Both are wrong in opposite directions. Neither is who you want on call at 2 a.m. when the error dashboard turns the colour of a traffic cone.</p><p>The candidate I want sits in the middle and sounds like this:</p><ul><li><p>&#8220;Yeah, my AI hallucinates sometimes. Here is how I usually catch it.&#8221;</p></li><li><p>&#8220;I keep my prompts short because I know the model loses track past a certain context length, and I would rather feed it one focused chunk than my whole repo.&#8221;</p></li><li><p>&#8220;I let it write boilerplate and unit tests. I do not let it near anything security-adjacent without reading every line.&#8221;</p></li><li><p>&#8220;The other day Claude confidently invented a method on RxJS that does not exist. I only caught it because the types screamed. If I had been vibe coding, that would have shipped.&#8221;</p></li><li><p>&#8220;I use Cursor for inline, Claude Code for bigger refactors, and I keep a list of things I never delegate. Yes, I have thought about this.&#8221;</p></li></ul><p>I do not need a lecture on transformer architecture. I need operational understanding. The same way I expect a frontend dev to know roughly what the event loop is doing without having read the V8 source code. Curious people ask themselves these questions unprompted. Incurious people treat the tool as a black box, and black boxes, as anyone who has been on-call can confirm, always explode at the worst possible moment.</p><p>If a candidate uses AI every day and cannot tell me why it sometimes lies, that is not a skill gap. That is an attitude problem. And attitude scales across a codebase faster than any framework.</p><h2>The checklist, because you are going to skim anyway</h2><p>Fine. Here, have it in one place:</p><ol><li><p><strong>The CV is theirs.</strong> Every bullet expands into a five-minute story. If it does not, it is not theirs.</p></li><li><p><strong>Decisions, not definitions.</strong> &#8220;Why did you choose this&#8221; beats &#8220;what is this&#8221; every time.</p></li><li><p><strong>Trade-offs with a take.</strong> &#8220;We picked X because Y, but today I would go with Z because...&#8221; That is the sound of a senior.</p></li><li><p><strong>Mistakes and the lessons.</strong> No mistakes means either they are lying or they never shipped anything that mattered.</p></li><li><p><strong>Tool understanding, AI included.</strong> How, why, where it breaks, what it cannot give them.</p></li><li><p><strong>Thinking out loud.</strong> I give a small problem, not to see the solution but to see the path. A solution without a path is a thing the LLM produced.</p></li><li><p><strong>Translation skills.</strong> &#8220;Explain this piece of your project to me like I am a PM.&#8221; Because the team is not just devs, and if you cannot talk to the people who pay you, you are a very expensive rubber duck.</p></li></ol><h2>If you are on the other side of the table</h2><p>Okay, switching sides. Hello. Statistically, some of you reading this are the people being interviewed, not the ones doing the interviewing. This section is for you, and I am going to be a little more direct than usual, because I think the advice going around the internet right now is making things worse.</p><p>Let me start with the single most important thing, and then we will get tactical.</p><h3>&#8220;I do not know&#8221; is a complete sentence</h3><p>I cannot stress this enough. The strongest thing you can say on a technical interview is &#8220;I do not know, but here is how I would find out.&#8221; The second you try to ad-lib your way through a question you do not understand, everyone on the call hears it. It is obvious. It is obvious the way a bad lie from a child is obvious. And the tragedy is that the question you were bluffing through was probably something I would have happily explained, moved past, and not held against you at all.</p><p>Seniors say &#8220;I do not know&#8221; constantly. It is one of the ways you tell a senior from a mid. Juniors say &#8220;I do not know&#8221; and then apologise for 30 seconds. Mids say &#8220;well, it kind of depends on, um, several factors, and in some contexts you might consider...&#8221; until the clock runs out. Be the senior, even if you are not one yet. The shortcut is available.</p><h3>Do not scheme</h3><p>I know the temptation. Everybody is whispering about earpieces, second monitors, AI overlays that read the screen and feed you answers, entire YouTube tutorials on &#8220;how to pass a technical interview with Cursor in the background.&#8221; Some of it even works, for about ten minutes. Here is what you need to understand: the hiring manager on the other side has done this hundreds of times and is specifically trained to notice the smell of a bluff. You are not as subtle as you think you are. Nobody is.</p><p>But more importantly, even if it works, what have you won? A job where every day is a new test you are going to fail because you lied yourself into it. A team that thinks you are a senior when you are a junior who is great at prompting. Six months of sweating every stand-up. At some point the mask slips, and then the fall is much harder than if you had just been honest in the first hour.</p><p>The market is tight right now, and I know how it feels to need the job. I am not lecturing you. I am telling you the scheme has a worse ROI than just being yourself. Truly.</p><h3>The behavioural stuff nobody writes about because it feels beneath them</h3><p>These feel embarrassingly basic to even list, which is exactly why I have to list them. Nobody writes about this stuff, so everybody gets it wrong, and then they go home and blame their rejection on &#8220;the vibes.&#8221; The vibes are downstream of the basics. Here are the basics.</p><ul><li><p><strong>Look at the camera, not at your own face.</strong> We have all been guilty of this one. Your own face is fascinating. It is right there, in the corner of the Zoom window, doing expressions you did not know it could do. But if your eyes are constantly darting sideways or down-and-to-the-right, we notice, and we notice in a very specific &#8220;is this person reading off something&#8221; way. If you need a moment to think, it is fine to look up or away briefly, the way people do in actual conversations. What sets off alarms is the rhythmic sideways scan of a person subtitling their own life in real time. Close the other tabs. Close the AI overlay you told yourself you would not actually use. You know the one. Yes, that one.</p></li><li><p><strong>Let silence exist.</strong> When I ask a hard question, take five seconds. Take ten. Say &#8220;give me a moment to think about that&#8221; and then actually think. I love hearing that sentence. It is the opposite of a red flag, it is basically a heart emoji. Rushing to fill silence is what gets you in trouble, because the words that come out under panic are almost always wrong, and then you have to defend them, and now you are in a bar fight with yourself over an opinion you did not even have ninety seconds ago.</p></li><li><p><strong>Do not memorise answers.</strong> Memorised answers have a specific cadence and we can all hear it. They sound like a LinkedIn post being read aloud by a person who used to do local radio, and nobody wants to hire a LinkedIn post. Your real experience, told messily, with detours and &#8220;wait no, that was the other project&#8221; corrections, is worth ten polished fakes. The mess is the credential.</p></li><li><p><strong>Disagree when you disagree.</strong> Politely, but actually disagree. If I suggest an approach and you think it is wrong, say so. This is not a trap. It is the single biggest signal that I want to work with you. The candidates I still remember years later are the ones who pushed back on something I said and were right. The ones I have already forgotten are the ones who nodded along and called everything I said &#8220;a great point.&#8221;</p></li><li><p><strong>Bring real questions.</strong> &#8220;What does a normal day look like&#8221; is fine, forgettable, and makes me think you downloaded a list. &#8220;What is the worst part of this codebase and why&#8221; is the kind of question that makes me want to hire you before we have even finished the call, because it is the question of a person who has already mentally moved in and is checking for damp patches. You are allowed to interview us back. You are encouraged to. If the company cannot handle being interviewed, run.</p></li><li><p><strong>Do not oversell AI fluency you do not have.</strong> If you have barely touched Cursor, do not pretend you live inside it like a monk in a cave. If Copilot is basically your whole AI story, say so, and then tell me what you got out of it and the one time it steered you off a cliff. Honest beats impressive every single time in this specific conversation, because I will find out in two follow-up questions either way, and the difference between &#8220;honest and learning&#8221; and &#8220;bluffing and caught&#8221; is the entire interview.</p></li></ul><h3>The CV and prep bit</h3><p>Since we are already here:</p><ul><li><p><strong>Read your CV out loud the day before.</strong> Every line should produce a memory, not a shrug. If a line makes you sweat, either cut it or prepare the real story behind it. You are not obligated to keep anything an AI tool put there &#8220;for impact.&#8221;</p></li><li><p><strong>Have two or three war stories in your pocket.</strong> Context, problem, decision you made, trade-off you accepted, outcome, what you would change. Do not recite them. Know them well enough that you can pull the relevant one out on demand when a question lands nearby. This is the thing seniors actually do that juniors think is improvisation. It is not. It is preparation that looks like improvisation.</p></li><li><p><strong>Know your own AI workflow, in your own words.</strong> Not &#8220;I use AI a lot.&#8221; That phrase is meaningless now. What specifically, for what, where do you not let it in, what was the last time it burned you. One minute of honest, specific answer is worth an hour of buzzwords.</p></li><li><p><strong>Be ready to talk about what the model cannot do.</strong> Not NeurIPS level. &#8220;Here is a time it lied to me and here is how I noticed&#8221; level. This is where you prove you are a pilot, not a passenger.</p></li><li><p><strong>Have opinions. Yours.</strong> Not mine, not Twitter&#8217;s, not the top answer on Stack Overflow. If I say something that genuinely lands, update your view in real time and tell me so. That is not weakness. That is the single most senior move in the entire interview.</p></li></ul><p>And one last thing, which is going to sound obvious until you realise how often people skip it: <strong>be kind to yourself about the interview you just bombed.</strong> Everybody bombs them. I bomb them. The person interviewing you has bombed them. The market is brutal right now and a rejection is usually not a referendum on your worth, it is a referendum on a one-hour conversation between two tired humans on a Tuesday afternoon. Take the notes, find the one thing you want to do better next time, and move on. That is the loop. Welcome to it.</p><div><hr></div><p>And if you are on my side of the table, one last word before we part: please, for the love of everything, stop asking questions whose answers live on the first page of a Google search. That was a decent filter in 2015 when the filter was the candidate&#8217;s memory. The memory now lives in a data centre in Virginia. You have to be the filter yourself. Sorry. That is the job now.</p><h2>Wrapping up before I start ranting again</h2><p>Hiring in the age of AI is not harder because candidates cheat. It is harder because almost everything we used to test for has quietly become free. Memorised APIs. Encyclopedic knowledge of Array methods. The ability to recite the Gang of Four without giggling at &#8220;Abstract Factory.&#8221; All of it, two tokens and a small monthly subscription.</p><p>The value of a candidate has moved. I wrote about this in <a href="https://www.better-frontend.dev/p/youre-not-a-coder-anymore-youre-a">&#8220;You&#8217;re Not a Coder Anymore. You&#8217;re a Solver&#8221;</a> and I stand by every word: the job has shifted toward the things models are worst at. Context. Judgement. Taste. Hard-won scars. The ability to explain to a non-technical stakeholder why we are about to spend two sprints on something that, to them, &#8220;already works.&#8221; If you can read those qualities off a person in an hour, you will build a good team. If you are still opening your interviews with &#8220;what is a Promise&#8221;, you will build a team that looks great on paper and turns into a small-but-reliable fire the first time production has a bad Tuesday.</p><p>So, hiring managers: stop outsourcing the filter to questions that Google solved a decade ago and ChatGPT now solves in three seconds while also writing you a haiku about it.</p><p>Candidates: stop scheming, start saying &#8220;I do not know&#8221;, and for the love of the event loop, close the other tab. We can tell.</p><p>And everybody else: welcome to 2026, where the most important interview skill is being a recognisable human being, which is, objectively, the most ridiculous sentence I have ever had to write on a professional blog. I blame the robots. It is definitely their fault.</p>]]></content:encoded></item><item><title><![CDATA[GSD Changed Everything. Here's How I Took It One Step Further]]></title><description><![CDATA[Or: Why I split my AI brain across two models and accidentally became a CEO]]></description><link>https://www.better-frontend.dev/p/escaping-the-gsd-communication-trap</link><guid isPermaLink="false">https://www.better-frontend.dev/p/escaping-the-gsd-communication-trap</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Tue, 03 Mar 2026 20:05:41 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Tgf_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe2f947-62d2-4e50-849c-6f097ae96925_3000x2000.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Tgf_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe2f947-62d2-4e50-849c-6f097ae96925_3000x2000.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Tgf_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe2f947-62d2-4e50-849c-6f097ae96925_3000x2000.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Tgf_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe2f947-62d2-4e50-849c-6f097ae96925_3000x2000.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Tgf_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe2f947-62d2-4e50-849c-6f097ae96925_3000x2000.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Tgf_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe2f947-62d2-4e50-849c-6f097ae96925_3000x2000.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Tgf_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe2f947-62d2-4e50-849c-6f097ae96925_3000x2000.jpeg" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bfe2f947-62d2-4e50-849c-6f097ae96925_3000x2000.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;A futuristic humanoid robot uses a computer in a contemporary office.&quot;,&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="A futuristic humanoid robot uses a computer in a contemporary office." title="A futuristic humanoid robot uses a computer in a contemporary office." srcset="https://substackcdn.com/image/fetch/$s_!Tgf_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe2f947-62d2-4e50-849c-6f097ae96925_3000x2000.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Tgf_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe2f947-62d2-4e50-849c-6f097ae96925_3000x2000.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Tgf_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe2f947-62d2-4e50-849c-6f097ae96925_3000x2000.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Tgf_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbfe2f947-62d2-4e50-849c-6f097ae96925_3000x2000.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@gettyimages">Getty Images</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>Let&#8217;s get one thing straight right out of the gate: the GSD movement is the best thing that happened to AI-assisted development this year. If you&#8217;ve been in any builder community recently, you&#8217;ve seen frameworks like GSD, Ralph Loop, BMAD, and Superpowers popping up everywhere. And for good reason. They solved the thing that was killing us all.</p><p>You know the thing. You sit down with Claude or ChatGPT, start a project, and for the first 30 minutes, you feel like a wizard. Code flows. Architecture emerges. You&#8217;re shipping features faster than you can drink your coffee.</p><p>Then, three hours later, the AI is &#8220;helpfully&#8221; refactoring your state management because something in your chat history from two hours ago confused it. Your global store now thinks &#8220;water intake&#8221; and &#8220;bicep curl max&#8221; are the same thing. You&#8217;re not engineering anymore. You&#8217;re babysitting.</p><p>GSD and similar frameworks tackled this problem head-on with a brilliant insight:&nbsp;<strong>stop dragging context through a single massive conversation.</strong> Instead, break work into atomic tasks, give each task a fresh context window, and let a lightweight orchestrator manage the flow. The orchestrator stays lean (around 15% context usage), while each executor gets a pristine 200K token workspace. Task 50 runs with the same quality as Task 1.</p><p>It works. It really works. Engineers at Amazon, Google, and Shopify are using it. It turned vibe coding from a gamble into something resembling a repeatable process.</p><p>So why am I writing this article?</p><p>Because after spending weeks using this pattern, I found a seam. A place where it can go further. And it starts with a question that none of these frameworks explicitly address:</p><p><strong>What if the orchestrator and the executor shouldn&#8217;t be the same model?</strong></p><h2>The Blind Spot in Single-Model Orchestration</h2><p>Here&#8217;s the thing about GSD and its siblings. They solved context rot beautifully. Fresh contexts, atomic commits, spec-driven planning. Chef&#8217;s kiss.</p><p>But there&#8217;s a subtlety hidden in the architecture: the orchestrator and the executors are typically the same model. Claude plans the work, Claude does the work, and Claude verifies the work. Different instances, sure. Clean contexts, absolutely. But the same &#8220;brain.&#8221;</p><p>Think about what that means in practice. When the planner creates a task spec, it does so with a particular understanding of code patterns, naming conventions, and architectural trade-offs. When the executor picks up that spec, it brings the exact same biases and blind spots.</p><p>It&#8217;s like having your CTO and your senior developer be the same person wearing different hats. Sure, they&#8217;ll be consistent. But will they catch their own assumptions? Will they challenge their own architectural instincts?</p><p>I started noticing this in my own work. The plans were solid. The execution was clean. But I kept finding myself in situations where both the plan and the code shared the same subtle misunderstanding of my project&#8217;s constraints. Nobody in the loop was offering a second opinion.</p><h2>The Architect-Executor Pattern: Splitting the Brain</h2><p>So I ran an experiment. Instead of letting a single model handle everything, I split the roles among different models. Not just different contexts. Different <em>brains</em>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1608444331927-05052bef8884?fm=jpg&amp;q=60&amp;w=3000&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1608444331927-05052bef8884?fm=jpg&amp;q=60&amp;w=3000&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D 424w, https://images.unsplash.com/photo-1608444331927-05052bef8884?fm=jpg&amp;q=60&amp;w=3000&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D 848w, https://images.unsplash.com/photo-1608444331927-05052bef8884?fm=jpg&amp;q=60&amp;w=3000&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D 1272w, https://images.unsplash.com/photo-1608444331927-05052bef8884?fm=jpg&amp;q=60&amp;w=3000&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1608444331927-05052bef8884?fm=jpg&amp;q=60&amp;w=3000&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" width="3000" height="2000" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1608444331927-05052bef8884?fm=jpg&amp;q=60&amp;w=3000&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2000,&quot;width&quot;:3000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;black and white bird flying over the building during daytime&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="black and white bird flying over the building during daytime" title="black and white bird flying over the building during daytime" srcset="https://images.unsplash.com/photo-1608444331927-05052bef8884?fm=jpg&amp;q=60&amp;w=3000&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D 424w, https://images.unsplash.com/photo-1608444331927-05052bef8884?fm=jpg&amp;q=60&amp;w=3000&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D 848w, https://images.unsplash.com/photo-1608444331927-05052bef8884?fm=jpg&amp;q=60&amp;w=3000&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D 1272w, https://images.unsplash.com/photo-1608444331927-05052bef8884?fm=jpg&amp;q=60&amp;w=3000&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@bdp028">Beckett P</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><h3>Role 1: The Architect (a different model than your executor)</h3><p>The Architect lives in your codebase. It has a live view of your file tree, your dependencies, and your actual data shapes. But here&#8217;s the key rule: <strong>the Architect does not write code.</strong></p><p>Instead, it thinks at the system level. You throw business problems at it, not coding tasks.</p><p>&#8220;We need to add a dynamic &#8216;Add Set&#8217; button to the workout screen, but it needs to tie into the local Zustand store without breaking the existing mocks.&#8221;</p><p>The Architect reads your files, figures out the dependency chain, and writes a living blueprint. I use a literal <code>implementation_plan.md</code> and a devlog file that grow alongside the project. The Architect is the keeper of the state. It documents what&#8217;s been built, what broke, what technical debt exists, and what the next move should be.</p><p>Here&#8217;s what makes this different from GSD&#8217;s planning phase: the Architect adapts <em>continuously</em>. When it looks at your store and realizes that your mock data is an array rather than a dictionary, it adjusts the plan <em>before</em> any code is written. It&#8217;s not a one-shot spec. It&#8217;s a living conversation with your codebase.</p><p>Using a different model family for this role gives you genuine architectural diversity. Different training data, different reasoning patterns, different instincts about what &#8220;good code&#8221; looks like. That tension is a feature, not a bug.</p><h3>Role 2: The Executor (your coding powerhouse)</h3><p>This is where the GSD principle shines hardest, and I kept it: <strong>the Executor gets a clean context every single time.</strong></p><p>Because the Architect maintains the high-level docs and project state in Markdown files, the Executor doesn&#8217;t need to remember yesterday&#8217;s conversation. It doesn&#8217;t need to remember anything.</p><p>The Architect produces a single, perfectly parameterized, atomic prompt. A clean instruction set with all the relevant context baked in. The Executor receives it in a brand new session, sees only the pure context needed for that specific feature, writes the code, commits to Git, and hands back a report.</p><p>Zero context degradation. Zero hallucinated dependencies from three hours ago. Zero &#8220;helpful&#8221; refactors you didn&#8217;t ask for.</p><h3>Role 3: You. The Loop Closer.</h3><p>You take the Executor&#8217;s report, pass it back to the Architect for type-checking and verification, and if it passes, you move on.</p><p>You&#8217;re no longer doing the prompt-tweak-paste-pray cycle. You&#8217;re not micromanaging syntax. You&#8217;re not re-reading a 200-message chat history every morning to remember where you left off.</p><p>You&#8217;ve become the CEO of your own codebase.</p><h2>What This Gives You Beyond GSD</h2><p>Let me be specific about where this extends what GSD already does well:</p><p><strong>Architectural diversity.</strong> GSD uses one model family for everything. My pattern deliberately uses different models for thinking vs. doing. The Architect might catch an assumption that the Executor would blindly follow, and vice versa. It&#8217;s the AI equivalent of code review by someone who doesn&#8217;t share your mental model.</p><p><strong>Model agnosticism.</strong> GSD is built around Claude Code (and recently OpenCode and Gemini CLI). My pattern is a methodology, not a framework. You can use whatever combination of models makes sense for your project. Gemini for architecture, Claude for execution. Or swap them. Or use local models for the Architect if you&#8217;re working with sensitive code.</p><p><strong>Continuous adaptation vs. phase-based planning.</strong> GSD&#8217;s planning phase produces specs that executors follow. My Architect is a living entity that watches the codebase change and adjusts the plan between every execution cycle. If the Executor&#8217;s commit reveals something unexpected, the Architect sees it in the file tree and recalibrates.</p><p><strong>Lightweight setup.</strong> GSD has commands, agents, workflows, and a <code>.planning/</code> directory structure. That&#8217;s great for larger projects. But sometimes you just need a second brain watching your file tree while you ship features. The Architect-Executor pattern works with nothing more than two chat windows and a shared repo.</p><p><strong>Built-in documentation as a side effect.</strong> Because the Architect maintains <code>implementation_plan.md</code> and a devlog, you get living project documentation for free. Not as a separate step or ceremony, but as a natural byproduct of how the system works.</p><h2>When to Use What</h2><p>Let me be honest about trade-offs, because this isn&#8217;t a silver bullet.</p><p><strong>Use GSD (or similar frameworks) when:</strong> you&#8217;re a solo developer who wants a reliable, automated pipeline. You&#8217;re starting a greenfield project. You want atomic git commits and automated verification out of the box. You want something that &#8220;just works&#8221; with a few commands. GSD is genuinely excellent for this.</p><p><strong>Use the Architect-Executor pattern when:</strong> you&#8217;re building something non-trivial where architectural decisions compound. You&#8217;ve noticed your AI making the same category of mistakes across planning and execution. You want a second opinion baked into your workflow. You&#8217;re comfortable with a more manual, methodology-driven approach. Or you simply want more visibility into what&#8217;s happening in your project without having to read chat history.</p><p><strong>Combine them.</strong> Nothing stops you from using GSD for execution while running a separate Architect model that reviews the <code>.planning/</code> directory and suggests adjustments. The patterns are complementary, not competing.</p><h2>The Bigger Picture</h2><p>Here&#8217;s what I think is actually happening in our industry right now. The initial wave of AI-assisted development was &#8220;just prompt it and see what happens.&#8221; That was exciting, but chaotic. The GSD wave brought structure: specs, clean contexts, atomic execution, verification loops. That was a massive leap forward.</p><p>The next wave, and I think we&#8217;re just at the beginning of it, is about <strong>heterogeneous AI systems.</strong> No single model does everything; different models play different roles, challenging each other and compensating for each other&#8217;s weaknesses. The same way a real engineering team works.</p><p>We don&#8217;t hire a team of five identical engineers and put them in separate rooms. We hire people with different backgrounds, different perspectives, and different instincts. Then we put them in the same room and let the creative friction produce something better than any one of them could build alone.</p><p>That&#8217;s what I&#8217;m chasing with the Architect-Executor pattern. Not a replacement for GSD. An evolution of the same core insight: <strong>structure makes AI reliable.</strong> I&#8217;m just adding one more layer of structure: the structure of diverse thinking.</p><p>Give it a try. Your codebase (and your mental health) will thank you.</p><p>Now, if you&#8217;ll excuse me, my Architect just told my Executor to replace the app icon with a picture of the Terminator, and I need to go verify that they haven&#8217;t accidentally triggered Skynet.</p><p>Keep building, friends. &#128640;</p>]]></content:encoded></item><item><title><![CDATA[You're Not a Coder Anymore. You're a Solver. Welcome to 2026.]]></title><description><![CDATA[How domain knowledge, business fluency, and curiosity became your most valuable technical skills]]></description><link>https://www.better-frontend.dev/p/youre-not-a-coder-anymore-youre-a</link><guid isPermaLink="false">https://www.better-frontend.dev/p/youre-not-a-coder-anymore-youre-a</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Tue, 24 Feb 2026 10:25:43 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1763568258235-f40425a94af9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxhaSUyMGNvZGluZyUyMGRldmVsb3BlcnxlbnwwfHx8fDE3NzE5MjQ4Njd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" 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://images.unsplash.com/photo-1763568258235-f40425a94af9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxhaSUyMGNvZGluZyUyMGRldmVsb3BlcnxlbnwwfHx8fDE3NzE5MjQ4Njd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1763568258235-f40425a94af9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxhaSUyMGNvZGluZyUyMGRldmVsb3BlcnxlbnwwfHx8fDE3NzE5MjQ4Njd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1763568258235-f40425a94af9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxhaSUyMGNvZGluZyUyMGRldmVsb3BlcnxlbnwwfHx8fDE3NzE5MjQ4Njd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1763568258235-f40425a94af9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxhaSUyMGNvZGluZyUyMGRldmVsb3BlcnxlbnwwfHx8fDE3NzE5MjQ4Njd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1763568258235-f40425a94af9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxhaSUyMGNvZGluZyUyMGRldmVsb3BlcnxlbnwwfHx8fDE3NzE5MjQ4Njd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1763568258235-f40425a94af9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxhaSUyMGNvZGluZyUyMGRldmVsb3BlcnxlbnwwfHx8fDE3NzE5MjQ4Njd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="6048" height="4032" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1763568258235-f40425a94af9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxhaSUyMGNvZGluZyUyMGRldmVsb3BlcnxlbnwwfHx8fDE3NzE5MjQ4Njd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:4032,&quot;width&quot;:6048,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;Computer screen displaying code with a context menu.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&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="Computer screen displaying code with a context menu." title="Computer screen displaying code with a context menu." srcset="https://images.unsplash.com/photo-1763568258235-f40425a94af9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxhaSUyMGNvZGluZyUyMGRldmVsb3BlcnxlbnwwfHx8fDE3NzE5MjQ4Njd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1763568258235-f40425a94af9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxhaSUyMGNvZGluZyUyMGRldmVsb3BlcnxlbnwwfHx8fDE3NzE5MjQ4Njd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1763568258235-f40425a94af9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxhaSUyMGNvZGluZyUyMGRldmVsb3BlcnxlbnwwfHx8fDE3NzE5MjQ4Njd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1763568258235-f40425a94af9?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxhaSUyMGNvZGluZyUyMGRldmVsb3BlcnxlbnwwfHx8fDE3NzE5MjQ4Njd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 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><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@dkomow">Daniil Komov</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p></p><p>Spotify&#8217;s co-CEO dropped something during an earnings call a few weeks ago that should have stopped every developer mid-scroll. His most senior engineers, the best people they have, haven&#8217;t written a single line of code since December. Not burnout. Not sabbatical. They just don&#8217;t need to.</p><p>They&#8217;re shipping production features to the iOS app from Slack, on their morning commute, before reaching the office.</p><p>There is no longer a reasonable debate about whether this changes how we work. The question worth asking is: if typing code is increasingly not the job, what is? And the even harder question underneath that: <em>who are you, if not someone who writes code?</em></p><h2>Wait, does any of this actually work, though?</h2><p>Before the existential crisis fully kicks in, let&#8217;s be honest about something.</p><p>The same week Spotify is celebrating its AI-powered commute deployment, engineering teams are quietly patching production issues introduced by their AI agent three PRs ago. There are developers who let Claude generate an entire module, merge it because the tests passed, and found out two weeks later that the tests were also generated and were testing the wrong thing entirely.</p><p>This is real. Confident, wrong AI output is arguably harder to catch than uncertain, wrong human output, because at least junior developers occasionally write &#8220;// not sure about this&#8221; in their commits. The AI never writes that. It ships with full conviction and impeccable formatting.</p><h3><strong>So is the problem the tools, or the people using them?</strong></h3><p>Honestly, probably both. But I&#8217;d lean harder on the second one. The tools have gotten dramatically better. What hasn&#8217;t kept up is the practice of using them well. Most developers picked up AI coding the same way they picked up Stack Overflow in 2012: copy, paste, pray, move on. That worked fine when the output was a 12-line function. It works less well when the output is a complete authentication flow.</p><p>The irony is that AI rewards exactly the engineering discipline people tend to skip when they&#8217;re moving fast. Clear specs. Real tests. Actually, reading the code before merging. When you skip those things while coding manually, you pay a small tax. When you skip them while directing an AI agent, you pay compound interest.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1565687981296-535f09db714e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wZXIlMjBsb29raW5nJTIwYXQlMjBjb2RlJTIwc2NyZWVufGVufDB8fHx8MTc3MTkyNTAzMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1565687981296-535f09db714e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wZXIlMjBsb29raW5nJTIwYXQlMjBjb2RlJTIwc2NyZWVufGVufDB8fHx8MTc3MTkyNTAzMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1565687981296-535f09db714e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wZXIlMjBsb29raW5nJTIwYXQlMjBjb2RlJTIwc2NyZWVufGVufDB8fHx8MTc3MTkyNTAzMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1565687981296-535f09db714e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wZXIlMjBsb29raW5nJTIwYXQlMjBjb2RlJTIwc2NyZWVufGVufDB8fHx8MTc3MTkyNTAzMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1565687981296-535f09db714e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wZXIlMjBsb29raW5nJTIwYXQlMjBjb2RlJTIwc2NyZWVufGVufDB8fHx8MTc3MTkyNTAzMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1565687981296-535f09db714e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wZXIlMjBsb29raW5nJTIwYXQlMjBjb2RlJTIwc2NyZWVufGVufDB8fHx8MTc3MTkyNTAzMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="4000" height="2667" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1565687981296-535f09db714e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wZXIlMjBsb29raW5nJTIwYXQlMjBjb2RlJTIwc2NyZWVufGVufDB8fHx8MTc3MTkyNTAzMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2667,&quot;width&quot;:4000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;man in black shirt using laptop computer and flat screen monitor&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&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="man in black shirt using laptop computer and flat screen monitor" title="man in black shirt using laptop computer and flat screen monitor" srcset="https://images.unsplash.com/photo-1565687981296-535f09db714e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wZXIlMjBsb29raW5nJTIwYXQlMjBjb2RlJTIwc2NyZWVufGVufDB8fHx8MTc3MTkyNTAzMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1565687981296-535f09db714e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wZXIlMjBsb29raW5nJTIwYXQlMjBjb2RlJTIwc2NyZWVufGVufDB8fHx8MTc3MTkyNTAzMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1565687981296-535f09db714e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wZXIlMjBsb29raW5nJTIwYXQlMjBjb2RlJTIwc2NyZWVufGVufDB8fHx8MTc3MTkyNTAzMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1565687981296-535f09db714e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wZXIlMjBsb29raW5nJTIwYXQlMjBjb2RlJTIwc2NyZWVufGVufDB8fHx8MTc3MTkyNTAzMXww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@vantaymedia">Van Tay Media</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p></p><h2>The part I keep watching happen and cringing</h2><p>Senior developers, experienced people, using AI and merging generated code without really reading it. The PR is green. The review is a formality. And nobody catches the subtle wrong assumption buried in the middle of 200 lines of perfectly formatted TypeScript.</p><p>That&#8217;s not an AI problem. That&#8217;s a thinking problem in an AI costume.</p><p>Nate Jones made a sharp observation about this: everyone is so busy debating whether AI replaces or augments engineers that nobody is asking what engineering actually is. And when you ask that question seriously, you realize the stuff AI can&#8217;t do is exactly what engineers are supposed to be for in the first place.</p><h2>From coder to solver. What does that actually mean?</h2><p>Here&#8217;s the reframe that helps me think about this clearly.</p><p>A coder&#8217;s value is in the output: working code, fast, correct. An AI agent is a better coder than most of us, in most situations, right now. That&#8217;s just true, and pretending otherwise is wasted energy.</p><p>A solver&#8217;s value is in the input: understanding what problem actually needs to be solved, for whom, under what constraints, with what tradeoffs. That&#8217;s something completely different. And it&#8217;s something AI is genuinely bad at, because it requires context that doesn&#8217;t live in a codebase.</p><p>Nate&#8217;s broader point is that the barrier to building something has shifted. It&#8217;s no longer syntax or knowing the right APIs. It&#8217;s curiosity about the actual problem, comfort with ambiguity, and the willingness to iterate toward something that makes sense. Those are more like liberal arts skills than computer science skills. Which is uncomfortable to say, but also kind of interesting.</p><p>What does that look like in practice for a senior developer?</p><h3><strong>Domain knowledge over framework knowledge</strong></h3><p>Knowing how your industry actually works, how your users think, and what constraints your business operates under is increasingly what makes your AI sessions produce something useful rather than something generic. The agent can generate a booking system. Only you know that &#8220;booking&#8221; in your context means three different things depending on which team is reading the code.</p><h3><strong>Business fluency is a technical skill</strong></h3><p>The developer who can sit in a product conversation, understand the actual goal behind a feature request, and push back with &#8220;that&#8217;s solving the wrong problem&#8221; is worth more in 2026 than the developer who can implement whatever spec lands on their plate faster than anyone. One of those developers is a coder. The other is a solver.</p><h3><strong>Curiosity about the problem, not just the implementation.</strong></h3><p>This is Nate&#8217;s point, and it&#8217;s the hardest one to operationalize. It means spending more time before the first prompt asking, &#8220;What are we actually trying to do here?&#8221; It means being the person who notices the feature request doesn&#8217;t match the user&#8217;s actual behavior. It means investing in understanding the domain more than the tooling, because the tooling will change again in six months anyway.</p><h3><strong>Soft skills that are actually hard.</strong></h3><p>Stakeholder conversations. Knowing when to push back on scope. Reading a room when the technical direction is wrong, and people don&#8217;t want to hear it. These have always mattered andhave been consistently undervalued because you couldn&#8217;t put them in a commit. In 2026, they&#8217;re what differentiates the people AI amplifies from the people AI replaces.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1639755507638-e34150b56db2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wJTIwcHJldmlldyUyMHNoaXAlMjBrZXlib2FyZCUyMG11Z3xlbnwwfHx8fDE3NzE5MjU0NTd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1639755507638-e34150b56db2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wJTIwcHJldmlldyUyMHNoaXAlMjBrZXlib2FyZCUyMG11Z3xlbnwwfHx8fDE3NzE5MjU0NTd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1639755507638-e34150b56db2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wJTIwcHJldmlldyUyMHNoaXAlMjBrZXlib2FyZCUyMG11Z3xlbnwwfHx8fDE3NzE5MjU0NTd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1639755507638-e34150b56db2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wJTIwcHJldmlldyUyMHNoaXAlMjBrZXlib2FyZCUyMG11Z3xlbnwwfHx8fDE3NzE5MjU0NTd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1639755507638-e34150b56db2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wJTIwcHJldmlldyUyMHNoaXAlMjBrZXlib2FyZCUyMG11Z3xlbnwwfHx8fDE3NzE5MjU0NTd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1639755507638-e34150b56db2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wJTIwcHJldmlldyUyMHNoaXAlMjBrZXlib2FyZCUyMG11Z3xlbnwwfHx8fDE3NzE5MjU0NTd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="4000" height="6000" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1639755507638-e34150b56db2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wJTIwcHJldmlldyUyMHNoaXAlMjBrZXlib2FyZCUyMG11Z3xlbnwwfHx8fDE3NzE5MjU0NTd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:6000,&quot;width&quot;:4000,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;a person holding a coffee mug in front of a keyboard&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&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="a person holding a coffee mug in front of a keyboard" title="a person holding a coffee mug in front of a keyboard" srcset="https://images.unsplash.com/photo-1639755507638-e34150b56db2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wJTIwcHJldmlldyUyMHNoaXAlMjBrZXlib2FyZCUyMG11Z3xlbnwwfHx8fDE3NzE5MjU0NTd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1639755507638-e34150b56db2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wJTIwcHJldmlldyUyMHNoaXAlMjBrZXlib2FyZCUyMG11Z3xlbnwwfHx8fDE3NzE5MjU0NTd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1639755507638-e34150b56db2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wJTIwcHJldmlldyUyMHNoaXAlMjBrZXlib2FyZCUyMG11Z3xlbnwwfHx8fDE3NzE5MjU0NTd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1639755507638-e34150b56db2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxfHxkZXZlbG9wJTIwcHJldmlldyUyMHNoaXAlMjBrZXlib2FyZCUyMG11Z3xlbnwwfHx8fDE3NzE5MjU0NTd8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@kvncnls">Kevin Canlas</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p></p><h2>Finding yourself as a senior in 2026</h2><p>Many senior developers built their confidence on being good at writing code. On knowing the framework deeply. On being the person in the room who understood what was actually happening under the hood.</p><p>That identity still matters. But it&#8217;s getting separated from the day-to-day of shipping things, which is a genuinely weird feeling that doesn&#8217;t have a clean resolution. Nobody really talks about it.</p><p>What seems to work is shifting the identity from &#8220;person who writes good code&#8221; to &#8220;person who produces correct outcomes.&#8221; The tool changed. The standard didn&#8217;t. You&#8217;re still the one responsible for what goes to production, still the one who knows whether a feature is solving the right problem, still the one who catches the architectural decision that will make everyone miserable in eighteen months.</p><p>If anything, that responsibility got heavier, not lighter. There&#8217;s more code moving faster, with less friction to slow you down when the direction is wrong.</p><p>The developers pulling ahead right now aren&#8217;t the purists refusing to touch AI. They&#8217;re not the ones merging without reading, either. They&#8217;re the ones who figured out that the interface between their thinking and the machine&#8217;s output is where the actual work lives, and invested accordingly.</p><blockquote><p>Not in the tools. In the thinking.</p></blockquote><p>Which, when you put it that way, has always been the job.</p>]]></content:encoded></item><item><title><![CDATA[Stop Vibing, Start Understanding: Why Your AI Assistant Needs Control (And How to Become Its Boss)]]></title><description><![CDATA[Or: How I Learned to Stop Prompting and Actually Understand My Code]]></description><link>https://www.better-frontend.dev/p/stop-vibing-start-understanding-why</link><guid isPermaLink="false">https://www.better-frontend.dev/p/stop-vibing-start-understanding-why</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Tue, 02 Dec 2025 10:35:59 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b61dd62f-f218-4bbe-a132-fbd048fb3ea5_2816x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So I just got back from the AI Poland conference, still buzzing from all the &#8220;AI will change everything&#8221; talks, and naturally dove straight into Addy Osmani&#8217;s latest piece about conductors and orchestrators in AI coding. My immediate reaction was &#8220;HELL YEAH, LET&#8217;S GOOO!&#8221; Because who doesn&#8217;t want an army of AI agents writing code while we sip coffee and pretend to be tech leads?&#8203;</p><p>But then reality slapped me in the face like a failed production deployment at 4 PM on Friday.</p><p>Between the conference hype and reading about these orchestrator workflows, I started reflecting on what I&#8217;m actually seeing in day-to-day development. And honestly? We need to have an uncomfortable conversation about what I&#8217;m calling &#8220;<strong>The Magic Wand Syndrome</strong>&#8221;: developers who think their AI coding assistant is literally magic that... makes correct code appear.&#8203;</p><p><strong>Spoiler alert: It&#8217;s not. You&#8217;re not a wizard. And that spell you just cast? Yeah, it summoned a demon that&#8217;s gonna haunt your codebase for the next six months.</strong></p><h2>The Vibe Coding Epidemic</h2><p>Let me paint you a picture. It&#8217;s Tuesday morning. Someone pushes a PR. Beautiful, clean-looking code. Merges through. Everyone&#8217;s happy.</p><p>Wednesday afternoon: Production is on fire. The person who wrote it? &#8220;Uh... I mean... the AI generated it, and the tests passed, so...&#8221;</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sTgM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd69193d4-3e95-480a-8fa8-3e4127702391_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sTgM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd69193d4-3e95-480a-8fa8-3e4127702391_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!sTgM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd69193d4-3e95-480a-8fa8-3e4127702391_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!sTgM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd69193d4-3e95-480a-8fa8-3e4127702391_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!sTgM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd69193d4-3e95-480a-8fa8-3e4127702391_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sTgM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd69193d4-3e95-480a-8fa8-3e4127702391_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d69193d4-3e95-480a-8fa8-3e4127702391_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:175833,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.better-frontend.dev/i/180487371?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd69193d4-3e95-480a-8fa8-3e4127702391_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!sTgM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd69193d4-3e95-480a-8fa8-3e4127702391_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!sTgM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd69193d4-3e95-480a-8fa8-3e4127702391_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!sTgM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd69193d4-3e95-480a-8fa8-3e4127702391_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!sTgM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd69193d4-3e95-480a-8fa8-3e4127702391_1024x559.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This is what industry folks are calling &#8220;<a href="https://flatlogic.com/blog/what-s-the-problem-with-vibe-coding-honest-review/">vibe coding</a>&#8220; and honestly? The name is perfect. It&#8217;s like coding by <em>vibes only</em>: no architecture, no understanding, &#8220;AI give me feature plz&#8221; and then YOLO&#8217;ing it into main.&#8203;</p><p>Here&#8217;s the thing that makes me want to scream into a pillow: <strong>I&#8217;m seeing PRs from people who literally cannot explain what their own code does</strong>. Not because it&#8217;s complex. Because they never actually <em>read</em> it. The AI wrote it; the tests went green (somehow). Ship it! &#128640;&#8203;</p><p>And look, I get it. We&#8217;ve all been there late at night trying to finish a ticket, prompting AI with increasingly desperate variations of the same question until <em>something</em> works. But that&#8217;s not development; that&#8217;s just guessing with extra steps and a $20/month subscription.</p><p>Recent observations show a troubling pattern: developers input entire problems into AI tools, receive partially functional code, then struggle to debug it or explain their design decisions. They try to fix issues with vague commands like &#8220;refactor the code&#8221; without providing meaningful context.&#8203;</p><h2>The &#8220;House of Cards&#8221; We&#8217;re All Building</h2><p>Fun fact that&#8217;ll keep you up at night: <a href="https://www.okoone.com/spark/technology-innovation/why-ai-generated-code-is-creating-a-technical-debt-nightmare/">GitClear analyzed 211 million lines of code</a> and found that AI-driven development is creating technical debt faster than anyone&#8217;s ever seen. Veterans with 35+ years of experience are basically going &#8220;what the actual hell&#8221; at how quickly codebases are turning into unmaintainable messes.&#8203;</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mje0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fe2d642-261b-41a7-a23e-c28c042070b0_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mje0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fe2d642-261b-41a7-a23e-c28c042070b0_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!mje0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fe2d642-261b-41a7-a23e-c28c042070b0_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!mje0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fe2d642-261b-41a7-a23e-c28c042070b0_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!mje0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fe2d642-261b-41a7-a23e-c28c042070b0_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mje0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fe2d642-261b-41a7-a23e-c28c042070b0_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9fe2d642-261b-41a7-a23e-c28c042070b0_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:181788,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.better-frontend.dev/i/180487371?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fe2d642-261b-41a7-a23e-c28c042070b0_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mje0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fe2d642-261b-41a7-a23e-c28c042070b0_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!mje0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fe2d642-261b-41a7-a23e-c28c042070b0_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!mje0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fe2d642-261b-41a7-a23e-c28c042070b0_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!mje0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9fe2d642-261b-41a7-a23e-c28c042070b0_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The cycle goes like this:</p><ol><li><p>AI generates code (fast! shiny! works... kinda!)</p></li><li><p>You accept it without fully understanding it</p></li><li><p>Subtle bugs emerge (weird edge cases, performance issues, that thing that only breaks in Safari)</p></li><li><p>Use AI to patch the bugs</p></li><li><p>Patches create new complexity</p></li><li><p>More patches, more complexity, more bandaids</p></li><li><p>Wake up six months later with a Frankenstein codebase held together by AI-generated duct tape</p></li></ol><p>And here&#8217;s the kicker: <strong>67% of developers now spend more time debugging AI-generated code than they save from initial generation</strong>. Another 68% spend more time resolving security vulnerabilities. Research shows 40% of AI-generated code contains vulnerabilities, with only 55% being secure enough for production.&#8203;</p><p>So what exactly are we optimizing for here? Carpal tunnel prevention? Because it&#8217;s certainly not &#8220;sustainable software development.&#8221;</p><h2>The Experience Gap Nobody Talks About</h2><p>Here&#8217;s an uncomfortable truth from recent research: AI coding tools help experienced developers WAY more than junior ones.&#8203;</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OAGO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F165d70aa-219b-4d45-a9b3-2d34971c52d9_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OAGO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F165d70aa-219b-4d45-a9b3-2d34971c52d9_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!OAGO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F165d70aa-219b-4d45-a9b3-2d34971c52d9_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!OAGO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F165d70aa-219b-4d45-a9b3-2d34971c52d9_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!OAGO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F165d70aa-219b-4d45-a9b3-2d34971c52d9_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OAGO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F165d70aa-219b-4d45-a9b3-2d34971c52d9_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/165d70aa-219b-4d45-a9b3-2d34971c52d9_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:181291,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.better-frontend.dev/i/180487371?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F165d70aa-219b-4d45-a9b3-2d34971c52d9_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!OAGO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F165d70aa-219b-4d45-a9b3-2d34971c52d9_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!OAGO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F165d70aa-219b-4d45-a9b3-2d34971c52d9_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!OAGO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F165d70aa-219b-4d45-a9b3-2d34971c52d9_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!OAGO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F165d70aa-219b-4d45-a9b3-2d34971c52d9_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Why? Because seniors can look at AI output and immediately spot when it&#8217;s:</p><ul><li><p>Hallucinating logic that seems right but isn&#8217;t</p></li><li><p>Producing functionally correct but architecturally terrible code</p></li><li><p>Copy-pasting patterns that don&#8217;t fit the actual use case</p></li><li><p>Creating security vulnerabilities wrapped in helpful comments</p></li></ul><p>Meanwhile, juniors (and honestly, some not-so-juniors) are playing &#8220;AI trial-and-error roulette&#8221;: keep prompting with slight variations, never actually tracing the root cause, vibe until something compiles.</p><p>The difference? <strong>Foundational knowledge</strong>. Seniors aren&#8217;t magic either; they have the experience to spot when AI is confidently wrong.</p><h2>The Skills Crisis We&#8217;re Creating</h2><p>Here&#8217;s what keeps me up at night: <strong>If AI does 90% of our coding work, how do the next generation of developers learn the fundamentals needed to actually use AI effectively?</strong>&#8203;</p><p>It&#8217;s a chicken-and-egg problem. You need foundational knowledge to validate AI output. But if AI is doing all the work, when do you build that foundation?</p><p>Junior developers historically learned by doing the grunt work: building simple components, fixing small bugs, writing utility functions. That&#8217;s where you develop pattern recognition and learn what &#8220;good code&#8221; feels like.</p><p>But AI just... does all that now. So we&#8217;re creating a generation of developers who can prompt AI but can&#8217;t debug when the AI gets it wrong, which it does. Constantly.</p><p>The traditional learning path is disappearing. <a href="https://erigo.se/en/articles/AI-Outsourcing-and-the-skills-gap-we-cannot-afford">Junior roles that once served as training grounds are being automated away</a>. New CS graduates are struggling because they can generate code with AI but lack the judgment to know if it&#8217;s actually good. They&#8217;re caught in a paradox: they need experience to use AI effectively, but AI is eliminating the entry-level work that builds that experience.&#8203;</p><h2>Look, I&#8217;m Not Anti-AI</h2><p>Before you think I&#8217;m some Luddite typing this on a mechanical keyboard by candlelight: I use AI tools literally every single day. They&#8217;re phenomenal. Cursor, Claude, Windsurf; they&#8217;ve all saved me countless hours in my daily development tasks.</p><p>But here&#8217;s my approach, and why I&#8217;m not currently experiencing the &#8220;oh god, why is everything broken&#8221; phenomenon:</p><h2>1. Understand Before You Accept</h2><p>When AI generates code, I actually <em>read it</em>. Revolutionary concept, I know. I understand what it&#8217;s doing, why it&#8217;s doing it that way, and whether there&#8217;s a better approach.</p><p>If I can&#8217;t explain it to a rubber duck, I don&#8217;t ship it. Period.</p><h2>2. Small, Controlled Changes</h2><p>I&#8217;m not spinning up three AI agents to rebuild my entire feature set while I make coffee. I use AI for one thing at a time. Generate a component. Review it. Test it. Understand it. <em>Then</em> move on.&#8203;</p><p>This isn&#8217;t slower; it&#8217;s <em>sustainable</em>. Because I&#8217;m not spending next week debugging mysterious issues in code I don&#8217;t understand.</p><h2>3. Architecture First, Magic Second</h2><p>Before I even <em>think</em> about prompting an AI, I know:</p><ul><li><p>What problem am I solving</p></li><li><p>How it fits into the existing system</p></li><li><p>What the data flow looks like</p></li><li><p>What could potentially break</p></li></ul><p>The AI helps with <em>implementation</em>, not <em>design</em>. If you&#8217;re using AI to design your architecture, we need to have a different conversation (probably involving your tech lead).</p><h2>4. Tests Are Not Optional</h2><p>&#8220;But the AI wrote tests!&#8221; Cool. Did you <em>read</em> them? Do they actually test meaningful scenarios or just happy-path nonsense that gives you a false sense of security?</p><p>I&#8217;ve seen AI-generated test suites that would make a QA engineer weep. 100% code coverage, 0% actual validation.</p><h2>How to Actually Become an AI Supervisor (Not Just a Prompt Monkey)</h2><p>Okay, so you&#8217;re convinced that blindly accepting AI output is a bad idea. Great! But how do you actually transition from &#8220;vibe coder&#8221; to &#8220;effective AI supervisor&#8221;? Here&#8217;s what I&#8217;ve learned:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zcxu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78d75e33-7c7c-42ae-9eb3-e6e64d700880_1024x559.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zcxu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78d75e33-7c7c-42ae-9eb3-e6e64d700880_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!zcxu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78d75e33-7c7c-42ae-9eb3-e6e64d700880_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!zcxu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78d75e33-7c7c-42ae-9eb3-e6e64d700880_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!zcxu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78d75e33-7c7c-42ae-9eb3-e6e64d700880_1024x559.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zcxu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78d75e33-7c7c-42ae-9eb3-e6e64d700880_1024x559.jpeg" width="1024" height="559" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/78d75e33-7c7c-42ae-9eb3-e6e64d700880_1024x559.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:559,&quot;width&quot;:1024,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:122744,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.better-frontend.dev/i/180487371?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78d75e33-7c7c-42ae-9eb3-e6e64d700880_1024x559.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zcxu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78d75e33-7c7c-42ae-9eb3-e6e64d700880_1024x559.jpeg 424w, https://substackcdn.com/image/fetch/$s_!zcxu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78d75e33-7c7c-42ae-9eb3-e6e64d700880_1024x559.jpeg 848w, https://substackcdn.com/image/fetch/$s_!zcxu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78d75e33-7c7c-42ae-9eb3-e6e64d700880_1024x559.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!zcxu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78d75e33-7c7c-42ae-9eb3-e6e64d700880_1024x559.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>For Everyone Still Learning</h2><p><strong>Treat AI Like a Very Confident Junior Developer</strong><br>Because that&#8217;s precisely what it is. It confidently generates code that <em>looks</em> right but might be fundamentally flawed. Would you merge a junior&#8217;s PR without reviewing it? No? Then don&#8217;t do it with AI.</p><p><strong>Use AI as a Learning Tool, Not a Replacement</strong><br>Instead of asking &#8220;write me a function that does X&#8221;, try &#8220;explain how I would implement X, then show me an example&#8221;. This forces you to understand the approach before seeing the implementation. GitHub&#8217;s research shows that configuring AI tools to <em>teach</em> rather than <em>do</em> dramatically improves skill development.&#8203;</p><p><strong>Build Your Pattern Recognition</strong><br>Work through the AI&#8217;s suggestions line by line. Ask yourself:</p><ul><li><p>Why did it choose this approach?</p></li><li><p>What are the tradeoffs?</p></li><li><p>What edge cases might it have missed?</p></li><li><p>How would I have done this differently?</p></li></ul><p>This is how you build the intuition that separates effective AI users from prompt monkeys.</p><p><strong>Master One Thing Deeply Before Scaling</strong><br>Don&#8217;t jump straight to orchestrating multiple AI agents. Master working with one agent in conductor mode first. Understand how to give it good context, how to validate its output, and how to iterate when it gets things wrong. <em>Then</em> consider more advanced workflows.&#8203;</p><h2>For Experienced Devs Feeling the Pressure</h2><p><strong>Don&#8217;t Get Complacent</strong><br>Just because you <em>can</em> validate AI output today doesn&#8217;t mean you&#8217;ll be able to tomorrow if you stop practicing. Keep writing code manually for complex features. Your skills are like muscles; they atrophy without use.</p><p><strong>Create Validation Checklists</strong><br>Build yourself a mental (or actual) checklist for reviewing AI-generated code:</p><ul><li><p>Does it follow our architecture patterns?</p></li><li><p>Are there security implications?</p></li><li><p>Is it maintainable?</p></li><li><p>Does it handle errors properly?</p></li><li><p>Will the next person understand this?</p></li></ul><p><strong>Share Your Process</strong><br>If you&#8217;re effectively using AI tools, teach others <em>how</em> you&#8217;re doing it. Not just &#8220;I use Cursor,&#8221; but &#8220;Here&#8217;s how I scope tasks, validate output, and iterate on results.&#8221; The team that learns together doesn&#8217;t create technical debt together.</p><h2>For Teams and Tech Leads</h2><p><strong>Rethink Code Review for the AI Era</strong><br>Code review becomes exponentially more critical when AI generates the code. The bottleneck isn&#8217;t AI speed; it&#8217;s our ability to read and validate what it produces quickly.</p><p>Consider:</p><ul><li><p>Requiring explanations of AI-generated code in PR descriptions</p></li><li><p>Asking &#8220;Can you walk me through this?&#8221; in reviews</p></li><li><p>Flagging PRs that seem to be pure AI output for extra scrutiny</p></li></ul><p><strong>Create Safe Spaces for Learning</strong><br>Junior developers need opportunities to build foundational skills even as AI automates away traditional learning tasks. Consider:</p><ul><li><p>Dedicated &#8220;no AI&#8221; projects or features for skill building</p></li><li><p>Pairing juniors with seniors to learn validation techniques</p></li><li><p>Code review sessions focused on <em>understanding</em> not just <em>shipping</em></p></li></ul><p><strong>Establish AI Usage Guidelines</strong><br>Not &#8220;don&#8217;t use AI&#8221; but &#8220;here&#8217;s how we use AI responsibly&#8221;:</p><ul><li><p>Architecture and design decisions remain human</p></li><li><p>All AI output must be reviewed and understood</p></li><li><p>Security-critical code requires extra validation</p></li><li><p>Test quality is non-negotiable</p></li></ul><h2>The Long Game: Controlled Orchestration</h2><p>I&#8217;m not against the orchestrator model Addy describes. Multi-agent workflows have genuine potential. But we need guardrails:&#8203;</p><p><strong>Start as a Conductor</strong>: Master working with a single AI agent interactively before trying to manage multiple autonomous agents. Learn to give good prompts, validate output, and iterate effectively.</p><p><strong>Architecture Always First</strong>: Before delegating to AI agents, understand your system architecture deeply. Design the interfaces, define the boundaries, and map the data flows. The AI can help build the house, but you need to design it.</p><p><strong>Incremental Autonomy</strong>: Give AI agents increasing autonomy as <em>you</em> become better at validating their output. Don&#8217;t jump straight to fire-and-forget mode. Earn the right to trust your AI by proving you can catch its mistakes.</p><p><strong>Maintain Your Skills</strong>: Keep writing code manually for complex features. Your ability to evaluate AI output depends on maintaining your fundamental skills. The best orchestrators are still excellent musicians.</p><h2>The Uncomfortable Reality</h2><p>Here&#8217;s the thing nobody wants to say out loud: <strong>using AI effectively is more complex than writing code manually</strong>.</p><p>Wait, what?</p><p>Yeah. Writing code yourself is straightforward: you think, you type, you debug. Using AI effectively requires:</p><ul><li><p>Understanding the problem deeply enough to prompt correctly</p></li><li><p>Validating the solution for correctness and quality</p></li><li><p>Integrating it into your existing architecture</p></li><li><p>Debugging when things go wrong (which they will)</p></li><li><p>Explaining it to others on your team</p></li></ul><p>The AI doesn&#8217;t make you faster by reducing the cognitive load. It makes you faster by <em>amplifying</em> your existing skills. If those skills are weak, you&#8217;re just amplifying weakness.</p><p>This is why seniors see massive productivity gains and juniors... don&#8217;t. It&#8217;s not about access to better tools. It&#8217;s about having the foundation to use those tools effectively.</p><h2>So Where Do We Go From Here?</h2><p>The future Addy describes is coming whether we like it or not. AI coding assistants are getting more autonomous, more capable, more integrated into our workflows. The orchestrator model will become reality for many development tasks.&#8203;</p><p>But we can choose <em>how</em> we get there.</p><p>We can rush headlong into it, vibing our way to unmaintainable codebases and a generation of developers who can&#8217;t debug their own systems. Or we can be intentional: build skills, maintain standards, and treat AI as a powerful tool that requires expertise to wield effectively.</p><p>I know which path I&#8217;m choosing. And honestly? After the AI Poland conference and reading about these new orchestrator tools, I&#8217;m more convinced than ever that <strong>the developers who win aren&#8217;t the ones who use AI the most</strong>; they&#8217;re the ones who use it the best.</p><p>So slow down. Understand your code. Review those PRs. Question the AI&#8217;s decisions. Maintain your fundamentals.</p><p>Because when that production system breaks (and it will), the AI agent won&#8217;t be the one fixing it. You will. And you&#8217;d better understand how it works.</p><div><hr></div><p><strong>Further Reading:</strong></p><ul><li><p><a href="https://addyo.substack.com/p/conductors-to-orchestrators-the-future">Addy Osmani&#8217;s article on Conductors to Orchestrators</a>&#8203;</p></li><li><p><a href="https://www.okoone.com/spark/technology-innovation/why-ai-generated-code-is-creating-a-technical-debt-nightmare/">Why AI-Generated Code Is Creating a Technical Debt Nightmareokoone</a>&#8203;</p></li><li><p><a href="https://flatlogic.com/blog/what-s-the-problem-with-vibe-coding-honest-review/">What&#8217;s The Problem With Vibe Coding?</a>&#8203;</p></li></ul><div><hr></div><p><em>What&#8217;s your experience with AI coding tools? Seeing quality issues in your codebase? Drop your thoughts in the comments; this conversation is too meaningful to leave unfinished.</em></p><p><em>P.S. If you recognized yourself in this post, don&#8217;t worry, we&#8217;ve all been there. The first step is admitting you have a vibe coding problem. The second step is actually reading your PRs before you merge them. You got this.</em></p>]]></content:encoded></item><item><title><![CDATA[PFC #5 - Page-Feature Composition: Real-World Examples ]]></title><description><![CDATA[Complete dashboard implementation from start to finish]]></description><link>https://www.better-frontend.dev/p/pfc-5</link><guid isPermaLink="false">https://www.better-frontend.dev/p/pfc-5</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Thu, 13 Nov 2025 16:33:37 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/bc22f416-e79c-433f-9f18-0033c3bc6fbf_6000x4000.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>TL;DR</h2><p>See PFC in action with a real admin dashboard. Three features (user management, analytics, notifications) are composed on one page. Complete walkthrough of folder structure, feature implementation, facades, containers, and final page composition. This is what clean Angular architecture looks like in production.</p><div><hr></div><p>Welcome to the final post in the Page-Feature Composition series. We&#8217;ve covered <a href="https://better-frontend.dev/pfc-1">the philosophy</a>, <a href="https://better-frontend.dev/pfc-2">page orchestration</a>, <a href="https://better-frontend.dev/pfc-3">feature architecture</a>, and <a href="https://better-frontend.dev/pfc-4">migration strategies</a>.</p><p>Now let&#8217;s put it all together with a complete, real-world example.</p><p>We&#8217;re building an admin dashboard that displays:</p><ul><li><p>Recent users with quick actions</p></li><li><p>Analytics overview with charts</p></li><li><p>Real-time notifications feed</p></li></ul><p>Three features, one page, zero spaghetti code.</p><blockquote><p><em><strong>Series Context: This is the final part of the Page-Feature Composition series. If you haven&#8217;t read parts 1-4, start there for the foundational concepts. This post shows everything working together.</strong></em></p></blockquote><h2>The Dashboard Requirements</h2><p>Our admin dashboard needs to:</p><p><strong>Show recent users</strong></p><ul><li><p>Display the last 10 active users</p></li><li><p>Quick view/edit/delete actions</p></li><li><p>Click to navigate to full user management</p></li></ul><p><strong>Display analytics</strong></p><ul><li><p>Total users, active sessions, revenue</p></li><li><p>Simple chart showing trends</p></li><li><p>Refresh on demand</p></li></ul><p><strong>Show notifications</strong></p><ul><li><p>Real-time notification feed</p></li><li><p>Mark as read functionality</p></li><li><p>Badge count on unread items</p></li></ul><p><strong>Page responsibilities</strong></p><ul><li><p>Arrange features in a grid layout</p></li><li><p>Handle navigation when users click items</p></li><li><p>That&#8217;s it!</p></li></ul><p>Let&#8217;s build it.</p><h2>Project Structure</h2><p>Here&#8217;s our complete folder organization:</p><pre><code>src/app/
&#9500;&#9472;&#9472; features/
&#9474;   &#9500;&#9472;&#9472; user-management/
&#9474;   &#9474;   &#9500;&#9472;&#9472; components/
&#9474;   &#9474;   &#9474;   &#9492;&#9472;&#9472; user-card/
&#9474;   &#9474;   &#9500;&#9472;&#9472; containers/
&#9474;   &#9474;   &#9474;   &#9492;&#9472;&#9472; user-management-container/
&#9474;   &#9474;   &#9500;&#9472;&#9472; store/
&#9474;   &#9474;   &#9474;   &#9492;&#9472;&#9472; facade.ts
&#9474;   &#9474;   &#9500;&#9472;&#9472; services/
&#9474;   &#9474;   &#9474;   &#9492;&#9472;&#9472; user-api.service.ts
&#9474;   &#9474;   &#9500;&#9472;&#9472; models/
&#9474;   &#9474;   &#9474;   &#9492;&#9472;&#9472; user.interface.ts
&#9474;   &#9474;   &#9500;&#9472;&#9472; user-management.facade.ts
&#9474;   &#9474;   &#9500;&#9472;&#9472; user-management.module.ts
&#9474;   &#9474;   &#9492;&#9472;&#9472; index.ts
&#9474;   &#9474;
&#9474;   &#9500;&#9472;&#9472; analytics/
&#9474;   &#9474;   &#9500;&#9472;&#9472; components/
&#9474;   &#9474;   &#9474;   &#9492;&#9472;&#9472; stats-card/
&#9474;   &#9474;   &#9500;&#9472;&#9472; containers/
&#9474;   &#9474;   &#9474;   &#9492;&#9472;&#9472; analytics-container/
&#9474;   &#9474;   &#9500;&#9472;&#9472; store/
&#9474;   &#9474;   &#9500;&#9472;&#9472; services/
&#9474;   &#9474;   &#9500;&#9472;&#9472; analytics.facade.ts
&#9474;   &#9474;   &#9500;&#9472;&#9472; analytics.module.ts
&#9474;   &#9474;   &#9492;&#9472;&#9472; index.ts
&#9474;   &#9474;
&#9474;   &#9492;&#9472;&#9472; notifications/
&#9474;       &#9500;&#9472;&#9472; components/
&#9474;       &#9474;   &#9492;&#9472;&#9472; notification-item/
&#9474;       &#9500;&#9472;&#9472; containers/
&#9474;       &#9474;   &#9492;&#9472;&#9472; notifications-container/
&#9474;       &#9500;&#9472;&#9472; store/
&#9474;       &#9500;&#9472;&#9472; services/
&#9474;       &#9500;&#9472;&#9472; notifications.facade.ts
&#9474;       &#9500;&#9472;&#9472; notifications.module.ts
&#9474;       &#9492;&#9472;&#9472; index.ts
&#9474;
&#9500;&#9472;&#9472; pages/
&#9474;   &#9492;&#9472;&#9472; admin-dashboard/
&#9474;       &#9500;&#9472;&#9472; admin-dashboard.page.ts
&#9474;       &#9500;&#9472;&#9472; admin-dashboard.html
&#9474;       &#9500;&#9472;&#9472; admin-dashboard.scss
&#9474;       &#9492;&#9472;&#9472; admin-dashboard.module.ts
&#9474;
&#9492;&#9472;&#9472; shared/
    &#9492;&#9472;&#9472; components/</code></pre><p>Three focused features. One orchestrating page. Clean separation.</p><h2>Feature 1: User Management</h2><h3>What It Does</h3><p>Handles everything related to displaying and managing users in &#8220;compact&#8221; mode for the dashboard.</p><h3>Models</h3><p>If it&#8217;s being used only by the model, keep inside the feature; otherwise, move to shared (as in here)</p><pre><code>// shared/models/user.interface.ts
export interface User {
  id: string;
  name: string;
  email: string;
  avatar: string;
  lastActive: Date;
  status: &#8216;active&#8217; | &#8216;inactive&#8217;;
}</code></pre><h3>Store Facade (Internal)</h3><pre><code>// features/user-management/store/facade.ts
@Injectable({ providedIn: &#8216;root&#8217; })
export class UserStoreFacade {
  readonly users$ = this.store.select(selectUsers);
  readonly isLoading$ = this.store.select(selectIsLoading);

  constructor(private store: Store) {}

  loadRecentUsers(): void {
    this.store.dispatch(loadRecentUsers());
  }

  deleteUser(userId: string): void {
    this.store.dispatch(deleteUser({ userId }));
  }
}</code></pre><h3>Container</h3><pre><code>// features/user-management/containers/user-management-container/user-management.container.ts
@Component({
  selector: &#8216;app-user-management-container&#8217;,
  templateUrl: &#8216;./user-management.html&#8217;
})
export class UserManagementContainer implements OnInit {
  @Input() mode: &#8216;full&#8217; | &#8216;compact&#8217; = &#8216;full&#8217;;
  @Input() maxItems?: number;
  @Output() userSelected = new EventEmitter&lt;string&gt;();

  users$ = this.storeFacade.users$;
  isLoading$ = this.storeFacade.isLoading$;

  constructor(private storeFacade: UserStoreFacade) {}

  ngOnInit(): void {
    this.storeFacade.loadRecentUsers();
  }

  get displayUsers$() {
    return this.users$.pipe(
      map(users =&gt; this.maxItems ? users.slice(0, this.maxItems) : users)
    );
  }

  onUserClick(userId: string): void {
    this.userSelected.emit(userId);
  }

  onUserDelete(userId: string): void {
    if (confirm(&#8217;Delete this user?&#8217;)) {
      this.storeFacade.deleteUser(userId);
    }
  }
}</code></pre><h3>Presentational Component</h3><pre><code>// features/user-management/components/user-card/user-card.component.ts
@Component({
  selector: &#8216;app-user-card&#8217;,
  templateUrl: &#8216;./user-card.html&#8217;,
  styleUrls: [&#8217;./user-card.scss&#8217;]
})
export class UserCardComponent {
  @Input() user: User;
  @Input() compact = false;
  @Output() click = new EventEmitter&lt;string&gt;();
  @Output() delete = new EventEmitter&lt;string&gt;();

  onClick(): void {
    this.click.emit(this.user.id);
  }

  onDelete(event: Event): void {
    event.stopPropagation();
    this.delete.emit(this.user.id);
  }
}</code></pre><h3>Feature Facade (Public API)</h3><pre><code>// features/user-management/user-management.facade.ts
@Injectable()
export class UserManagementFacade {
  readonly isLoading$ = this.storeFacade.isLoading$;

  constructor(private storeFacade: UserStoreFacade) {}

  refreshUsers(): void {
    this.storeFacade.loadRecentUsers();
  }
}</code></pre><h3>Public Exports</h3><pre><code>// features/user-management/index.ts
export * from &#8216;./user-management.module&#8217;;
export * from &#8216;./user-management.facade&#8217;;</code></pre><p><strong>That&#8217;s the complete user-management feature.</strong> Self-contained, testable, reusable.</p><p></p><h2>Feature 2: Analytics (Simplified)</h2><h3>What It Does</h3><p>Displays key metrics and a simple chart. Handles data loading and formatting.</p><h3>Container</h3><pre><code>// features/analytics/containers/analytics-container/analytics.container.ts
@Component({
  selector: &#8216;app-analytics-container&#8217;,
  templateUrl: &#8216;./analytics.html&#8217;
})
export class AnalyticsContainer implements OnInit {
  @Input() mode: &#8216;full&#8217; | &#8216;summary&#8217; = &#8216;full&#8217;;

  stats$ = this.storeFacade.stats$;
  isLoading$ = this.storeFacade.isLoading$;

  constructor(private storeFacade: AnalyticsStoreFacade) {}

  ngOnInit(): void {
    this.storeFacade.loadStats();
  }

  refresh(): void {
    this.storeFacade.loadStats();
  }
}</code></pre><h3>Stats Card Component</h3><pre><code>// features/analytics/components/stats-card/stats-card.component.ts
@Component({
  selector: &#8216;app-stats-card&#8217;,
  templateUrl: &#8216;./stats-card.html&#8217;,
  styleUrls: [&#8217;./stats-card.scss&#8217;]
})
export class StatsCardComponent {
  @Input() label: string;
  @Input() value: number;
  @Input() icon: string;
  @Input() trend?: &#8216;up&#8217; | &#8216;down&#8217;;
}</code></pre><p><strong>The analytics feature is complete.</strong> Simple, focused, reusable.</p><p></p><h2>Feature 3: Notifications (Simplified)</h2><h3>What It Does</h3><p>Displays real-time notifications, allows marking as read, and shows unread count.</p><h3>Container</h3><pre><code>// features/notifications/containers/notifications-container/notifications.container.ts
@Component({
  selector: &#8216;app-notifications-container&#8217;,
  templateUrl: &#8216;./notifications.html&#8217;
})
export class NotificationsContainer implements OnInit {
  @Input() maxItems = 10;

  notifications$ = this.storeFacade.notifications$;
  unreadCount$ = this.storeFacade.unreadCount$;

  constructor(private storeFacade: NotificationsStoreFacade) {}

  ngOnInit(): void {
    this.storeFacade.loadNotifications();
  }

  get displayNotifications$() {
    return this.notifications$.pipe(
      map(items =&gt; items.slice(0, this.maxItems))
    );
  }

  onMarkAsRead(id: string): void {
    this.storeFacade.markAsRead(id);
  }

  onMarkAllRead(): void {
    this.storeFacade.markAllAsRead();
  }
}</code></pre><h3>Notification Item Component</h3><pre><code>// features/notifications/components/notification-item/notification-item.component.ts
@Component({
  selector: &#8216;app-notification-item&#8217;,
  templateUrl: &#8216;./notification-item.html&#8217;,
  styleUrls: [&#8217;./notification-item.scss&#8217;]
})
export class NotificationItemComponent {
  @Input() notification: Notification;
  @Output() markRead = new EventEmitter&lt;string&gt;();

  onMarkRead(): void {
    this.markRead.emit(this.notification.id);
  }
}</code></pre><p>The notifications feature is complete. Handles its own complexity.</p><h2>The Page: Bringing It All Together</h2><p>Now the magic happens. The page composes features.</p><h3>Page Component</h3><pre><code>// pages/admin-dashboard/admin-dashboard.page.ts
@Component({
  templateUrl: &#8216;./admin-dashboard.html&#8217;,
  styleUrls: [&#8217;./admin-dashboard.scss&#8217;]
})
export class AdminDashboardPage {
  constructor(private router: Router) {}

  onUserSelected(userId: string): void {
    this.router.navigate([&#8217;/users&#8217;, userId]);
  }
}</code></pre><p><strong>That&#8217;s it. 8 lines</strong>. No business logic. Just navigation.</p><h3>Page Template</h3><pre><code>&lt;!-- pages/admin-dashboard/admin-dashboard.html --&gt;
&lt;div class=&#8221;dashboard-layout&#8221;&gt;
  &lt;header class=&#8221;dashboard-header&#8221;&gt;
    &lt;h1&gt;Admin Dashboard&lt;/h1&gt;
    &lt;p&gt;Welcome back! Here&#8217;s your overview.&lt;/p&gt;
  &lt;/header&gt;

  &lt;div class=&#8221;dashboard-grid&#8221;&gt;
    &lt;!-- Analytics Feature --&gt;
    &lt;section class=&#8221;analytics-section&#8221;&gt;
      &lt;app-analytics-container mode=&#8221;summary&#8221;&gt;&lt;/app-analytics-container&gt;
    &lt;/section&gt;

    &lt;!-- User Management Feature --&gt;
    &lt;section class=&#8221;users-section&#8221;&gt;
      &lt;div class=&#8221;section-header&#8221;&gt;
        &lt;h2&gt;Recent Users&lt;/h2&gt;
        &lt;a routerLink=&#8221;/users&#8221;&gt;View all&lt;/a&gt;
      &lt;/div&gt;
      &lt;app-user-management-container
        mode=&#8221;compact&#8221;
        [maxItems]=&#8221;10&#8221;
        (userSelected)=&#8221;onUserSelected($event)&#8221;&gt;
      &lt;/app-user-management-container&gt;
    &lt;/section&gt;

    &lt;!-- Notifications Feature --&gt;
    &lt;section class=&#8221;notifications-section&#8221;&gt;
      &lt;div class=&#8221;section-header&#8221;&gt;
        &lt;h2&gt;Notifications&lt;/h2&gt;
        &lt;span class=&#8221;unread-badge&#8221;&gt;3&lt;/span&gt;
      &lt;/div&gt;
      &lt;app-notifications-container
        [maxItems]=&#8221;5&#8221;&gt;
      &lt;/app-notifications-container&gt;
    &lt;/section&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre><p><strong>Pure composition.</strong> Three features, one layout, zero logic.</p><h3>Page Styling</h3><pre><code>// pages/admin-dashboard/admin-dashboard.scss
.dashboard-layout {
  padding: 2rem;
}

.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2rem;
  
  @media (max-width: 1024px) {
    grid-template-columns: 1fr;
  }
}

.analytics-section {
  grid-column: 1 / -1; // Full width
}

.section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1rem;
}</code></pre><p>Layout is a page concern. Features don&#8217;t care where they&#8217;re placed.<br></p><h3>Page Module</h3><pre><code>// pages/admin-dashboard/admin-dashboard.module.ts
@NgModule({
  declarations: [AdminDashboardPage],
  imports: [
    CommonModule,
    RouterModule.forChild([
      { path: &#8216;&#8217;, component: AdminDashboardPage }
    ]),
    // Import feature modules
    UserManagementModule,
    AnalyticsModule,
    NotificationsModule
  ]
})
export class AdminDashboardModule { }</code></pre><p>Lazy-loaded page imports the features it needs.</p><h2>What We&#8217;ve Achieved</h2><p>Let&#8217;s look at what this architecture gives us:</p><h3>Reusability</h3><p><strong>User Management Feature</strong> can be used:<br>- Admin dashboard (compact mode, 10 users)<br>- Full user management page (full mode, all users)<br>- User selection dialog (selection mode)<br>- Profile sidebar (compact mode, 1 user)<br><br>Same feature, different contexts, zero modifications.<br><br><strong>Analytics Feature</strong> works on:<br>- Admin dashboard (summary mode)<br>- Analytics page (full mode with charts)<br>- Mobile app dashboard (summary mode)<br><br><strong>Notifications Feature</strong> appears:<br>- Dashboard (compact, 5 items)<br>- Header dropdown (compact, 3 items)<br>- Notifications page (full, all items)</p><h3>Testability</h3><pre><code>describe(&#8217;UserManagementContainer&#8217;, () =&gt; {
  it(&#8217;should load users on init&#8217;, () =&gt; {
    const facade = jasmine.createSpyObj(&#8217;UserStoreFacade&#8217;, [&#8217;loadRecentUsers&#8217;]);
    const container = new UserManagementContainer(facade);
    
    container.ngOnInit();
    
    expect(facade.loadRecentUsers).toHaveBeenCalled();
  });
});</code></pre><p><strong>Test page composition:</strong></p><pre><code>describe(&#8217;AdminDashboardPage&#8217;, () =&gt; {
  it(&#8217;should navigate when user selected&#8217;, () =&gt; {
    const router = jasmine.createSpyObj(&#8217;Router&#8217;, [&#8217;navigate&#8217;]);
    const page = new AdminDashboardPage(router);
    
    page.onUserSelected(&#8217;user-123&#8217;);
    
    expect(router.navigate).toHaveBeenCalledWith([&#8217;/users&#8217;, &#8216;user-123&#8217;]);
  });
});</code></pre><p>Clean, simple tests.</p><h3>Team Workflow</h3><p><br><strong>Parallel development:</strong><br>- Developer A: User management feature<br>- Developer B: Analytics feature  <br>- Developer C: Notifications feature<br>- Developer D: Dashboard page composition<br><br>No conflicts. No waiting. Pure parallelization.</p><h3>Maintainability</h3><p>Six months later, you need to add a filter to user management. Where do you look? The user-management feature. Not scattered across five different pages.<br><br>Need to update the analytics chart? Analytics feature. One place. Clear boundary.<br><br>Bug in notifications? Notifications feature. Isolated problem, isolated fix.<br></p><h2>Alternative Page Layouts</h2><p>Because features are composable, you can create different layouts easily.</p><h3><br>Mobile Dashboard (Vertical Stack)</h3><pre><code>&lt;!-- pages/mobile-dashboard/mobile-dashboard.html --&gt;
&lt;div class=&#8221;mobile-layout&#8221;&gt;
  &lt;app-analytics-container mode=&#8221;summary&#8221;&gt;&lt;/app-analytics-container&gt;
  &lt;app-notifications-container [maxItems]=&#8221;3&#8221;&gt;&lt;/app-notifications-container&gt;
  &lt;app-user-management-container mode=&#8221;compact&#8221; [maxItems]=&#8221;5&#8221;&gt;&lt;/app-user-management-container&gt;
&lt;/div&gt;</code></pre><p>Same features, different order, mobile-specific layout.</p><h3>Executive Dashboard (Different Focus)</h3><pre><code>&lt;!-- pages/executive-dashboard/executive-dashboard.html --&gt;
&lt;div class=&#8221;executive-layout&#8221;&gt;
  &lt;app-analytics-container mode=&#8221;full&#8221;&gt;&lt;/app-analytics-container&gt;
  &lt;!-- No users, no notifications - just analytics --&gt;
&lt;/div&gt;</code></pre><p>Pick and choose features as needed.<br></p><h3>Custom Client Dashboard</h3><pre><code>&lt;!-- pages/client-dashboard/client-dashboard.html --&gt;
&lt;div class=&#8221;client-layout&#8221;&gt;
  &lt;!-- Different features for client view --&gt;
  &lt;app-project-status-container&gt;&lt;/app-project-status-container&gt;
  &lt;app-billing-summary-container&gt;&lt;/app-billing-summary-container&gt;
  &lt;app-support-tickets-container&gt;&lt;/app-support-tickets-container&gt;
&lt;/div&gt;</code></pre><p>Create new dashboards by combining and customizing features.</p><p></p><h2>Key Patterns Demonstrated</h2><h3>Container/Component Split</h3><p>Containers handle logic and state. Components are pure presenters. This makes components highly reusable and easy to test.</p><h3>Facade Pattern</h3><p>Each feature exposes a simple facade. Pages use it for programmatic actions. Internal complexity stays hidden.</p><h3>Input/Output Communication</h3><p>Features expose inputs for configuration and outputs for events. Pages coordinate but don&#8217;t implement.</p><h3>Mode-Based Behavior</h3><p>Features support different modes via inputs. Same feature, different contexts, no code duplication.</p><h3>Lazy Loading</h3><p>Pages are lazy-loaded routes. Features are imported by pages that need them. Optimal bundle sizes.</p><h2>Common Questions</h2><p><em><strong>&#8221;Isn&#8217;t this over-engineered for a simple dashboard?&#8221;</strong></em><br><br>For a toy project, yes. For a production app that will grow, no. The structure supports growth without complexity explosion.<br><br><em><strong>&#8221;What if features need to share data?&#8221;</strong></em><br><br>Let the page coordinate. Feature A emits event, page passes data to Feature B via input. Keep features independent.<br><br><em><strong>&#8221;Can features use each other&#8217;s components?&#8221;</strong></em><br><br>No. Features are independent. If you need shared components, put them in `shared/`. If two features overlap significantly, maybe they&#8217;re one feature.<br><br><em><strong>&#8221;How do I handle authentication?&#8221;</strong></em><br><br>Core concern, not a feature. Put it in `core/auth/`. Features can inject auth services when needed.<br><br><em><strong>&#8221;What about shared utilities?&#8221;</strong></em><br><br>``shared/` folder. Pipes, utility functions, truly generic components. Not business-domain-specific.<br></p><h2>Summary</h2><p>This is what PFC looks like in production. Three focused features. One orchestrating page. Clean separation. Easy testing. True reusability.<br><br><strong>What we built:</strong><br>&#9989; User management feature (complete CRUD capability)  <br>&#9989; Analytics feature (stats and charts)  <br>&#9989; Notifications feature (real-time feed)  <br>&#9989; Admin dashboard page (pure composition)  <br>&#9989; Alternative layouts (mobile, executive, client)<br><br><strong>What we achieved:</strong><br>&#9989; Features work in multiple contexts without modification  <br>&#9989; Page is &lt;10 lines of business logic  <br>&#9989; Tests are simple and isolated  <br>&#9989; Team can work in parallel  <br>&#9989; Maintenance is predictable<br><br><strong>What you should do next:</strong><br>Build one feature this way. See how it feels. Experience the benefits firsthand. Then build another. And another. Before you know it, you&#8217;ll have a scalable, maintainable Angular application.</p><h2>Series Wrap-Up</h2><p><br>We&#8217;ve covered a lot across five posts:<br><br>PFC #1: The philosophy and high-level structure  <br>PFC #2: Pages as orchestrators  <br>PFC #3: Building bulletproof features  <br>PFC #4: Migration strategies  <br>PFC #5: Complete real-world example<br><br>You now have everything you need to adopt PFC in your projects.<br><br>Start small. Build one feature. Show your team. Let the benefits speak for themselves.<br><br>Happy composing! &#127919;<br><br><strong>This concludes the Page-Feature Composition series. Follow<a href="https://better-frontend.dev"> better-frontend.dev</a> for more practical Angular architecture insights.</strong><br><br><strong>The Series:</strong><br>- PFC #1 - <a href="https://better-frontend.dev/pfc-1">The Philosophy</a><br>- PFC #2 - <a href="https://better-frontend.dev/pfc-2">Pages as Orchestrators</a><br>- PFC #3 - <a href="https://better-frontend.dev/pfc-3">Building Features</a><br>- PFC #4 - <a href="https://better-frontend.dev/pfc-4">Migration Strategies</a><br>- PFC #5 - <a href="https://better-frontend.dev/pfc-5">Real-World Examples</a> &#8592; You are here<br><br><br></p><p></p>]]></content:encoded></item><item><title><![CDATA[PFC #4 - Page-Feature Composition: From Monolith to Composition]]></title><description><![CDATA[Practical migration strategies without rewriting everything]]></description><link>https://www.better-frontend.dev/p/pfc-4</link><guid isPermaLink="false">https://www.better-frontend.dev/p/pfc-4</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Thu, 13 Nov 2025 15:54:30 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/46698752-fdcc-4d40-82ba-046048045b84_5049x3366.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>TL;DR</h2><p>You don&#8217;t need to rewrite your entire app to adopt PFC. Start with one new feature or refactor one messy page. Use the strangler pattern: build new features with PFC, gradually extract from monoliths, and let old and new coexist during migration. Most teams see value within 2-3 sprints without disrupting ongoing work.</p><div><hr></div><p>In&nbsp;<a href="https://better-frontend.dev/pfc-1">PFC #1</a>, <a href="https://better-frontend.dev/pfc-2">PFC #2</a>, and <a href="https://better-frontend.dev/pfc-3">PFC #3</a>, we covered the theory and architecture. Now comes the hard part: actually doing it.</p><p>You&#8217;ve got an existing codebase. Maybe 50 routes. Maybe 200 components. Maybe years of &#8220;temporary&#8221; solutions. The question isn&#8217;t &#8220;Should we adopt PFC?&#8221; It&#8217;s &#8220;How do we adopt PFC without stopping all feature development for six months?&#8221;</p><p>Good news: You don&#8217;t rewrite everything. You migrate gradually.</p><blockquote><p><em><strong>Series Context: This is Part 4 of the Page-Feature Composition series. This post assumes you understand PFC principles from parts 1-3. We&#8217;re focused purely on practical migration.</strong></em></p></blockquote><h2>The Core Philosophy: Divide and Conquer</h2><p>PFC is built on the ancient principle of &#8220;divide and conquer.&#8221;</p><p>The idea is simple: break complex problems into smaller, manageable, reusable pieces. Instead of one 2000-line component trying to do everything, you have focused features that each do one thing well.</p><p><strong>The beauty?</strong> Once you divide correctly:</p><ul><li><p>Features become truly reusable across different pages</p></li><li><p>Teams can work in parallel without conflicts</p></li><li><p>Testing becomes straightforward</p></li><li><p>Bugs have clear boundaries</p></li></ul><p>The trick is knowing <strong>how</strong> to divide. What makes a good feature boundary?</p><h2>How Big Should a Feature Be?</h2><p>This is the million-dollar question. The answer: <strong>start small and logically encapsulate one part of your app.</strong></p><h3>The Golden Rule</h3><p><strong>Too small is always better than too big.</strong></p><p>Seriously. A feature that&#8217;s too small can easily be merged with another later. A feature that&#8217;s too large becomes a mini-monolith, defeating the whole purpose of PFC.</p><p>When in doubt, go smaller. You can always combine features later if you realize they&#8217;re too granular. You can&#8217;t easily split a massive feature that&#8217;s already coupled to everything.</p><h3>What Makes a Good Feature?</h3><p>Think of features as <strong>logical business capabilities that can stand alone.</strong></p><p><strong>Good feature boundaries:</strong></p><p><strong>User Management</strong> - Handles all aspects of user management, including listing, creating, editing, deleting, and searching. Includes all user-related forms and displays. Can answer &#8220;Show me users&#8221; in any context.</p><p><strong>Product Catalog</strong> - Handles products: browse, filter, create, edit, and details. Includes product forms, cards, grids, and filters. Can answer &#8220;Show me products&#8221; in any context.</p><p><strong>Order Processing</strong> - Handles orders, including creation, viewing, updating status, and tracking. Includes order forms, order lists, and status displays. Can answer &#8220;Show me orders&#8221; in any context.</p><p>Each one is <strong>complete</strong> and <strong>independent</strong>. You could develop the user-management feature without the products existing. You could test it in isolation. You could use it on five different pages.</p><h3>Start Small, Grow Organically</h3><p>Don&#8217;t try to build the perfect feature structure on day one. Start with the smallest logical unit.</p><p><strong>Example: Building a shopping app</strong></p><p>Don&#8217;t start with one giant &#8220;e-commerce&#8221; feature handling products, cart, checkout, orders, and payments.</p><p>Start with small, focused features:</p><ul><li><p>Product catalog feature (just products)</p></li><li><p>Then add the  shopping cart feature (just the cart)</p></li><li><p>Then add checkout feature (just checkout flow)</p></li><li><p>Then add the order history feature (just past orders)</p></li></ul><p>Four focused features consistently outperform one massive feature.</p><h3>The &#8220;Elevator Pitch&#8221; Test</h3><p>Can you describe what the feature does in one sentence without using &#8220;and&#8221;?</p><p>&#9989; <strong>Good:</strong> &#8220;User management handles all user CRUD operations&#8221;</p><p>&#10060; <strong>Too big:</strong> &#8220;E-commerce handles products, shopping cart, checkout, orders, and payments&#8221;</p><p>If you need &#8220;and&#8221;, you probably have multiple features hiding in there.</p><h3>The &#8220;Can It Live Alone?&#8221; Test</h3><p>Before creating a feature, ask:</p><ol><li><p><strong>Can I develop this without other features existing?</strong> Yes &#8594; Good feature</p></li><li><p><strong>Can I test this in complete isolation?</strong> Yes &#8594; Good feature</p></li><li><p><strong>Can I use this on multiple different pages?</strong> Yes &#8594; Good feature</p></li><li><p><strong>Does it handle ONE clear business domain?</strong> Yes &#8594; Good feature</p></li></ol><p>If you pass all four tests, you&#8217;ve found a reasonable feature boundary.</p><h3>Practical Sizing Advice</h3><p><strong>Start here:</strong></p><ul><li><p>3-7 components per feature</p></li><li><p>1-2 containers per feature</p></li><li><p>Focused on one business domain</p></li><li><p>Takes 1-2 sprints to build the initial version</p></li></ul><p><strong>If you&#8217;re not sure</strong>, err on the side of caution and choose the smaller option. You can always combine later.</p><p>Remember: <strong>Too small beats too big, every time.</strong></p><h2>The Mindset Shift</h2><p><strong>Stop thinking:</strong> &#8220;We need to refactor the whole app.&#8221;</p><p><strong>Start thinking:</strong> &#8220;What&#8217;s the smallest valuable change we can make?&#8221;</p><p>Migration isn&#8217;t a project. It&#8217;s a practice. You don&#8217;t set aside three months to &#8220;do PFC.&#8221; You adopt it incrementally, one feature at a time, while continuing to ship new features.</p><h2>The Strangler Pattern</h2><p>Named after strangler fig trees that gradually replace their host trees.</p><p><strong>The idea:</strong></p><ol><li><p>Build new features using PFC</p></li><li><p>Extract pieces from old monoliths when you touch them</p></li><li><p>Let old and new coexist peacefully</p></li><li><p>Over time, PFC becomes the majority</p></li></ol><p>No significant bang rewrite. No project plan. Just better decisions going forward.</p><h2>Migration Strategies</h2><p>Select the option that best suits your situation.</p><h3>Strategy 1: Start with New Features</h3><p><strong>Best for:</strong> Teams actively building new features</p><p>When you need to build something new, build it the PFC way. Create the features and pages folders alongside your existing code. New features go into the new structure. Old code stays where it is.</p><p><strong>Timeline:</strong> Immediate. No refactoring needed.</p><h3>Strategy 2: Extract One Messy Page</h3><p><strong>Best for:</strong> Teams with one particularly problematic page</p><p>Pick your messiest page component and identify the distinct business concerns it handles. Create a small, focused feature for each concern. Move the logic out of the page into features. Update the page to simply compose those features.</p><p><strong>Timeline:</strong> 1-2 sprints for one page.</p><h3>Strategy 3: Refactor When You Touch It</h3><p><strong>Best for:</strong> Teams with stable codebases, infrequent changes</p><p>The &#8220;Boy Scout Rule&#8221;: Leave code better than you found it. When fixing a bug or adding a feature, extract just the part you&#8217;re changing into a small feature. Leave the rest as-is. Over time, the page gets cleaner.</p><p><strong>Timeline:</strong> No dedicated time. Happens during normal work.</p><h2>Deconstructing the Kitchen Sink</h2><p>Let&#8217;s walk through extracting features from a monolithic component, one at a time.</p><h3>The Problem: Kitchen Sink Component</h3><p>Imagine your admin page: 500+ lines handling users, products, and orders. Everything mixed together. Services injected for all three domains. The state is scattered everywhere. Methods for every possible action.</p><p>It&#8217;s unmaintainable. Let&#8217;s fix it gradually.</p><h3>Step 1: Extract First Feature (Users)</h3><p><strong>Sprint 1:</strong> Focus only on users.</p><p>Create the <code>user-management</code> feature. Move all user-related logic from the page into the feature&#8217;s container. The container handles loading, users, searching, selecting, and deleting. All the user complexity now resides in the feature.</p><p>Update your page: replace all the inline user code with the user-management container component. Your page places the container in the template.</p><p><strong>Result:</strong> Your page component shrinks from 500 lines to ~350 lines. All user services and states have been removed from the page. Progress!</p><h3>Step 2: Extract Second Feature (Products)</h3><p><strong>Sprint 2:</strong> Now tackle products.</p><p>Create the <code>product-catalog</code> feature. Move all product-related logic into this feature&#8217;s container. Product loading, filtering, and editing - all moves to the feature.</p><p>Update your page again: replace the inline product code with the product-catalog container.</p><p><strong>Result:</strong> Page is now ~200 lines. Two features extracted. Getting cleaner!</p><h3>Step 3: Extract Third Feature (Orders)</h3><p><strong>Sprint 3:</strong> Finally, orders.</p><p>Create the <code>order-processing</code> feature. Move all order logic into the feature&#8217;s container.</p><p>Update the page one last time: replace the inline order code with the order-processing container.</p><p><strong>Result:</strong> Page is now ~50 lines. Almost nothing is left but orchestration.</p><h3>Step 4: Migrate to Clean Page Component</h3><p><strong>Sprint 4:</strong> Final cleanup.</p><p>Move the remaining orchestration logic to a new, clean page component in your <code>pages/</code> folder. The page just composes the three feature containers and handles navigation.</p><p>When a user is selected, the page navigates to the user detail route. When a product is clicked, route to product details. That&#8217;s all the page does.</p><p><strong>Final result:</strong> Went from 500+ lines to ~30 lines. Three focused features. Clean orchestration. Pure composition.</p><h2>Passing Context to Features</h2><p>Pages extract context from routes (like IDs) and pass them as inputs to features. The feature receives the ID, loads the data, and displays it. Clean separation.</p><p>Example: The user detail page retrieves the user ID from the route, passes it to the user-management container in &#8220;detail&#8221; mode with the ID. The feature handles the rest.</p><h2>Handling Legacy Dependencies</h2><h3>Bridging Old Services</h3><p>Your new features may need to utilize existing centralized services temporarily. That&#8217;s okay. Wrap the old service in your feature&#8217;s new service. Delegate to it temporarily. Replace the implementation later when you&#8217;re ready. No rush.</p><h3>Shared State</h3><p>Old and new code might need to share data during migration. Let them temporarily share the feature&#8217;s store. Document it as a TODO. Fix it when you can. Keep moving forward.</p><h3>Team Workflow</h3><p><strong>Clear Ownership:</strong> Small features mean one developer or pair owns each feature. Parallel development becomes natural.</p><p><strong>Code Review Focus:</strong> Verify that new features are concise, focused, and adhere to the PFC structure. For legacy code, encourage extraction but don&#8217;t demand perfection. Praise progress.</p><p><strong>Documentation:</strong> Create a simple one-page guide showing the feature template and basic rules. Keep it minimal.</p><h2>Common Challenges</h2><p><strong>&#8220;How do we decide feature boundaries?&#8221;</strong><br>Start small. Use the &#8220;Elevator Pitch&#8221; test. One sentence, no &#8220;and&#8221;. If it passes and can live alone, it&#8217;s good.</p><p><strong>&#8220;What if we make features too small?&#8221;</strong><br>Better than too big! You can combine later. You can&#8217;t easily split a massive feature.</p><p><strong>&#8220;Our codebase is too big.&#8221;</strong><br>Doesn&#8217;t matter. Start with one small feature. Prove value. Expand organically.</p><p><strong>&#8220;Team doesn&#8217;t know PFC&#8221;</strong><br>Share the posts. Extract one feature together. Learn by doing. Start small.</p><p><strong>&#8220;What about existing tests?&#8221;</strong><br>Keep them. They still test old code. Write new tests for new features. Delete old tests when old code is removed. Gradual transition.</p><h2>When NOT to Migrate</h2><p><strong>Skip if:</strong> App sunsetting soon, tiny, overwhelmed team, already well-organized, pure maintenance mode.</p><p><strong>Delay if:</strong> Major deadline approaching, team crisis</p><p>Timing matters.</p><h2>Summary</h2><p>Migration is a divide-and-conquer approach through small, focused features extracted one at a time.</p><p><strong>Key takeaways:</strong></p><p>&#9989; Start small - too small beats too big<br>&#9989; Extract one feature per sprint from monoliths<br>&#9989; Let old and new coexist during migration<br>&#9989; Move to clean page component when all features extracted<br>&#9989; Pass context (IDs, params) as inputs to features<br>&#9989; Use &#8220;Elevator Pitch&#8221; and &#8220;Can It Live Alone?&#8221; tests<br>&#9989; First feature is hardest; gets easier</p><p>Start tomorrow. Extract one small feature from your messiest page. Show value. Repeat.</p><h2>What&#8217;s Next</h2><p><strong><a href="https://www.better-frontend.dev/p/pfc-5">PFC #5 - Real-World Examples</a></strong><br>Complete implementation walkthrough. Dashboard with multiple features working together.</p><h2>Your Migration Plan</h2><ol><li><p>Which strategy? (New features / Extract page / Refactor on touch)</p></li><li><p>What&#8217;s your messiest page? (Start there!)</p></li><li><p>What&#8217;s the first small feature to extract? (Users? Products? Orders?)</p></li><li><p>Does it pass the tests? (Elevator pitch / Can it live alone)</p></li></ol><p>Share in the comments. Let&#8217;s learn together.</p><div><hr></div><p><em>Part 4 of the Page-Feature Composition series. Follow along at <a href="https://better-frontend.dev/">better-frontend.dev</a> for practical Angular architecture.</em></p><p><strong>Previous:</strong> <a href="https://better-frontend.dev/pfc-3">PFC #3 - Building Features</a><br><strong>Next:</strong> <a href="https://www.better-frontend.dev/p/pfc-5">PFC #5 - Real-World Examples</a></p><p></p><p><strong>This concludes the Page-Feature Composition series. Follow<a href="https://better-frontend.dev/"> better-frontend.dev</a> for more practical Angular architecture insights.</strong><br><br><strong>The Series:</strong><br>- PFC #1 - <a href="https://better-frontend.dev/pfc-1">The Philosophy</a><br>- PFC #2 - <a href="https://better-frontend.dev/pfc-2">Pages as Orchestrators</a><br>- PFC #3 - <a href="https://better-frontend.dev/pfc-3">Building Features</a><br>- PFC #4 - <a href="https://better-frontend.dev/pfc-4">Migration Strategies</a> &#8592; You are here<br>- PFC #5 - <a href="https://better-frontend.dev/pfc-5">Real-World Examples</a></p>]]></content:encoded></item><item><title><![CDATA[PFC #3 - Page-Feature Composition: Building Bulletproof Features]]></title><description><![CDATA[The internal architecture that actually scales]]></description><link>https://www.better-frontend.dev/p/pfc-3</link><guid isPermaLink="false">https://www.better-frontend.dev/p/pfc-3</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Thu, 13 Nov 2025 13:27:49 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ff954417-1b03-4d45-96ab-5c0a87993ea7_3086x2087.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>TL;DR</h2><p>Features are self-contained mini-applications. Use the container/component pattern (smart/dumb), expose a clean facade, keep internal services private, and manage state with NgRx or signals. Only export what pages need via <code>index.ts</code>. This structure makes features truly reusable across different page contexts without modification.</p><div><hr></div><p>In <a href="https://better-frontend.dev/pfc-1">PFC #1</a>, we learned the philosophy. In <a href="https://better-frontend.dev/pfc-2">PFC #2</a>, we mastered page orchestration. Now it&#8217;s time for the main event: building features that actually work.</p><p>This is where the rubber meets the road. Get feature architecture right, and your app scales beautifully. Get it wrong, and you&#8217;re back to the spaghetti code we&#8217;re trying to escape.</p><blockquote><p><em><strong>Series Context: This is Part 3 of the Page-Feature Composition series. Parts 1 and 2 covered the &#8220;why&#8221; and &#8220;how pages work.&#8221; This post is all about &#8220;how features work internally.&#8221;</strong></em></p></blockquote><h2>The Feature Mission</h2><p>A feature has one job: <strong>to own a complete business capability</strong>.</p><p>Not &#8220;render a card.&#8221; Not &#8220;call an API.&#8221; Own the <strong>entire</strong> user management capability. Or product catalog. Or order processing. Everything related to that domain lives in one place.</p><h3>What Makes a Good Feature?</h3><ul><li><p><strong>Self-contained</strong> - Everything it needs lives inside the feature folder</p></li><li><p><strong>Focused</strong> - One business domain, not &#8220;everything users-related ever&#8221;</p></li><li><p><strong>Reusable</strong> - Works in different page contexts without modification</p></li><li><p><strong>Testable</strong> - Can be tested in isolation</p></li><li><p><strong>Encapsulated</strong> - Internal details hidden from pages</p></li></ul><p>Think of features as npm packages you could theoretically publish. They should be that independent.</p><h2>Feature Anatomy</h2><p>Let&#8217;s break down a real feature structure:</p><pre><code>features/user-management/
&#9500;&#9472;&#9472; components/                    # Dumb UI
&#9474;   &#9500;&#9472;&#9472; user-card/
&#9474;   &#9474;   &#9500;&#9472;&#9472; user-card.component.ts
&#9474;   &#9474;   &#9500;&#9472;&#9472; user-card.component.html
&#9474;   &#9474;   &#9492;&#9472;&#9472; user-card.component.scss
&#9474;   &#9492;&#9472;&#9472; user-form/
&#9500;&#9472;&#9472; containers/                    # Smart orchestrators
&#9474;   &#9492;&#9472;&#9472; user-management-container/
&#9474;       &#9500;&#9472;&#9472; user-management.container.ts
&#9474;       &#9492;&#9472;&#9472; user-management.html
&#9500;&#9472;&#9472; services/                      # Feature-specific services
&#9474;   &#9492;&#9472;&#9472; user-api.service.ts
&#9500;&#9472;&#9472; store/                         # State management
&#9474;   &#9500;&#9472;&#9472; actions.ts
&#9474;   &#9500;&#9472;&#9472; reducers.ts
&#9474;   &#9500;&#9472;&#9472; selectors.ts
&#9474;   &#9500;&#9472;&#9472; effects.ts
&#9474;   &#9492;&#9472;&#9472; facade.ts                  # Internal store wrapper
&#9500;&#9472;&#9472; models/
&#9474;   &#9492;&#9472;&#9472; user.interface.ts
&#9500;&#9472;&#9472; user-management.facade.ts      # PUBLIC API
&#9500;&#9472;&#9472; user-management.module.ts
&#9492;&#9472;&#9472; index.ts                       # Public exports only</code></pre><p>Each folder has a specific purpose. Let&#8217;s explore them.</p><h3>Containers vs Components</h3><p>This is the heart of feature architecture. Get this right, and everything else falls into place.</p><h3>Containers (Smart Components)</h3><p>Containers orchestrate feature logic. They know about services, state, and business rules.</p><pre><code>// src/app/features/user-management/containers/user-management-container/user-management.container.ts
import { Component, OnInit } from &#8216;@angular/core&#8217;;
import { Observable } from &#8216;rxjs&#8217;;

import { User } from &#8216;../../models/user.interface&#8217;;
import { UserStoreFacade } from &#8216;../../store/facade&#8217;;

@Component({
  selector: &#8216;app-user-management-container&#8217;,
  templateUrl: &#8216;./user-management.html&#8217;
})
export class UserManagementContainer implements OnInit {
  users$: Observable&lt;User[]&gt; = this.storeFacade.users$;
  isLoading$: Observable&lt;boolean&gt; = this.storeFacade.isLoading$;
  
  constructor(private storeFacade: UserStoreFacade) {}
  
  ngOnInit(): void {
    this.storeFacade.loadUsers();
  }
  
  onUserSelect(user: User): void {
    this.storeFacade.selectUser(user.id);
  }
  
  onUserDelete(user: User): void {
    if (confirm(`Delete ${user.name}?`)) {
      this.storeFacade.deleteUser(user.id);
    }
  }
}</code></pre><p><strong>Container responsibilities:</strong></p><ul><li><p>Fetch data via services/store</p></li><li><p>Handle user interactions (clicks, form submits)</p></li><li><p>Manage feature-level state</p></li><li><p>Coordinate multiple components</p></li><li><p>Expose observables to the template</p></li></ul><p><strong>One container per feature</strong> is usually enough. Sometimes you need two to three for complex features, but that&#8217;s rare.</p><h3>Components (Dumb/Presentational)</h3><p>Components render UI. No services, no state, just inputs and outputs.</p><pre><code>// src/app/features/user-management/components/user-card/user-card.component.ts
import { Component, EventEmitter, Input, Output } from &#8216;@angular/core&#8217;;
import { User } from &#8216;../../models/user.interface&#8217;;

@Component({
  selector: &#8216;app-user-card&#8217;,
  templateUrl: &#8216;./user-card.component.html&#8217;,
  styleUrls: [&#8217;./user-card.component.scss&#8217;]
})
export class UserCardComponent {
  @Input() user: User;
  @Input() showActions = true;
  
  @Output() select = new EventEmitter&lt;User&gt;();
  @Output() delete = new EventEmitter&lt;User&gt;();
  
  onSelect(): void {
    this.select.emit(this.user);
  }
  
  onDelete(): void {
    this.delete.emit(this.user);
  }
}</code></pre><pre><code>&lt;!-- user-card.component.html --&gt;
&lt;div class=&#8221;user-card&#8221; (click)=&#8221;onSelect()&#8221;&gt;
  &lt;img [src]=&#8221;user.avatar&#8221; [alt]=&#8221;user.name&#8221;&gt;
  &lt;div class=&#8221;user-info&#8221;&gt;
    &lt;h3&gt;{{ user.name }}&lt;/h3&gt;
    &lt;p&gt;{{ user.email }}&lt;/p&gt;
  &lt;/div&gt;
  &lt;button 
    *ngIf=&#8221;showActions&#8221;
    (click)=&#8221;onDelete(); $event.stopPropagation()&#8221;
    class=&#8221;delete-btn&#8221;&gt;
    Delete
  &lt;/button&gt;
&lt;/div&gt;</code></pre><p><strong>Component responsibilities:</strong></p><ul><li><p>Render UI based on inputs</p></li><li><p>Emit events on user interactions</p></li><li><p>Apply styling</p></li><li><p>Stay dumb and reusable</p></li></ul><p><strong>The Golden Rule:</strong> If a component injects a service, it&#8217;s not dumb anymore. Make it a container or refactor.</p><h3>The Template (Container)</h3><pre><code>&lt;!-- user-management.html --&gt;
&lt;div class=&#8221;user-management&#8221;&gt;
  &lt;div class=&#8221;loading&#8221; *ngIf=&#8221;isLoading$ | async&#8221;&gt;
    Loading users...
  &lt;/div&gt;
  
  &lt;div class=&#8221;user-list&#8221; *ngIf=&#8221;!(isLoading$ | async)&#8221;&gt;
    &lt;app-user-card
      *ngFor=&#8221;let user of users$ | async&#8221;
      [user]=&#8221;user&#8221;
      [showActions]=&#8221;true&#8221;
      (select)=&#8221;onUserSelect($event)&#8221;
      (delete)=&#8221;onUserDelete($event)&#8221;&gt;
    &lt;/app-user-card&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre><p>Container template coordinates components. That&#8217;s it.</p><h2>The Facade Pattern</h2><p>This is crucial. Every feature needs <strong>two</strong> facades:</p><h3>Internal Store Facade (Private)</h3><p>Wraps your state management (NgRx, signals, services). Used by containers inside the feature.</p><pre><code>// src/app/features/user-management/store/facade.ts
import { Injectable } from &#8216;@angular/core&#8217;;
import { Store } from &#8216;@ngrx/store&#8217;;
import { Observable } from &#8216;rxjs&#8217;;

import { User } from &#8216;../models/user.interface&#8217;;
import * as userActions from &#8216;./actions&#8217;;
import * as userSelectors from &#8216;./selectors&#8217;;
import { State } from &#8216;./reducers&#8217;;

@Injectable({ providedIn: &#8216;root&#8217; })
export class UserStoreFacade {
  readonly users$: Observable&lt;User[]&gt; = this.store.select(userSelectors.selectUsers);
  readonly isLoading$: Observable&lt;boolean&gt; = this.store.select(userSelectors.selectIsLoading);
  readonly selectedUser$: Observable&lt;User | null&gt; = this.store.select(userSelectors.selectSelectedUser);

  constructor(private store: Store&lt;State&gt;) {}

  loadUsers(): void {
    this.store.dispatch(userActions.loadUsers());
  }
  
  selectUser(userId: string): void {
    this.store.dispatch(userActions.selectUser({ userId }));
  }
  
  deleteUser(userId: string): void {
    this.store.dispatch(userActions.deleteUser({ userId }));
  }
}
</code></pre><p><strong>Not exported.</strong> Containers use this directly.</p><h3>External Feature Facade (Public)</h3><p>Simple API for pages. Wraps the internal facade.</p><pre><code>// src/app/features/user-management/user-management.facade.ts
import { Injectable } from &#8216;@angular/core&#8217;;
import { Observable } from &#8216;rxjs&#8217;;

import { UserStoreFacade } from &#8216;./store/facade&#8217;;

@Injectable()
export class UserManagementFacade {
  readonly isLoading$: Observable&lt;boolean&gt; = this.storeFacade.isLoading$;
  
  constructor(private storeFacade: UserStoreFacade) {}
  
  // Pages can call these if needed
  loadUsers(): void {
    this.storeFacade.loadUsers();
  }
  
  refreshUsers(): void {
    this.storeFacade.loadUsers();
  }
}</code></pre><p><strong>This gets exported.</strong> Pages can use it for programmatic actions.</p><h3>Why Two Facades?</h3><p><strong>Separation of concerns:</strong></p><ul><li><p>Internal facade = feature implementation details</p></li><li><p>External facade = public API contract</p></li></ul><p><strong>Flexibility:</strong></p><ul><li><p>Change internal state management (NgRx &#8594; signals) without breaking pages</p></li><li><p>Internal facade can be complex</p></li><li><p>External facade stays simple</p></li></ul><p><strong>Encapsulation:</strong></p><ul><li><p>Pages only see what you want them to see</p></li><li><p>Internal complexity hidden</p></li></ul><h2>State Management</h2><p>You have options. Pick what fits your needs.</p><h3>Option 1: NgRx (Recommended for Complex Features)</h3><p>Complete state management with actions, reducers, effects, selectors.</p><pre><code>// store/actions.ts
export const loadUsers = createAction(&#8217;[User Management] Load Users&#8217;);
export const loadUsersSuccess = createAction(
  &#8216;[User Management] Load Users Success&#8217;,
  props&lt;{ users: User[] }&gt;()
);

// store/reducers.ts
export const reducer = createReducer(
  initialState,
  on(loadUsers, state =&gt; ({ ...state, loading: true })),
  on(loadUsersSuccess, (state, { users }) =&gt; ({ ...state, users, loading: false }))
);

// store/effects.ts
loadUsers$ = createEffect(() =&gt;
  this.actions$.pipe(
    ofType(loadUsers),
    switchMap(() =&gt;
      this.userApi.getUsers().pipe(
        map(users =&gt; loadUsersSuccess({ users })),
        catchError(error =&gt; of(loadUsersFailure({ error })))
      )
    )
  )
);
</code></pre><p><strong>Use when:</strong></p><ul><li><p>Complex state with multiple data sources</p></li><li><p>Need time-travel debugging</p></li><li><p>Large team wants predictable patterns</p></li></ul><h3>Option 2: Signals (Simpler Alternative)</h3><p>Angular 16+ signals for reactive state.</p><pre><code>// store/user.store.ts
@Injectable({ providedIn: &#8216;root&#8217; })
export class UserStore {
  private usersSignal = signal&lt;User[]&gt;([]);
  private loadingSignal = signal(false);
  
  readonly users = this.usersSignal.asReadonly();
  readonly isLoading = this.loadingSignal.asReadonly();
  
  constructor(private userApi: UserApiService) {}
  
  loadUsers(): void {
    this.loadingSignal.set(true);
    this.userApi.getUsers().subscribe(users =&gt; {
      this.usersSignal.set(users);
      this.loadingSignal.set(false);
    });
  }
}</code></pre><p><strong>Use when:</strong></p><ul><li><p>Simpler state needs</p></li><li><p>Want less boilerplate</p></li><li><p>Working with Angular 16+</p></li></ul><h3>Option 3: Service with BehaviorSubject</h3><p>Classic reactive service pattern.</p><pre><code>// services/user-state.service.ts
@Injectable({ providedIn: &#8216;root&#8217; })
export class UserStateService {
  private usersSubject = new BehaviorSubject&lt;User[]&gt;([]);
  private loadingSubject = new BehaviorSubject(false);
  
  readonly users$ = this.usersSubject.asObservable();
  readonly isLoading$ = this.loadingSubject.asObservable();
  
  constructor(private userApi: UserApiService) {}
  
  loadUsers(): void {
    this.loadingSubject.next(true);
    this.userApi.getUsers().subscribe(users =&gt; {
      this.usersSubject.next(users);
      this.loadingSubject.next(false);
    });
  }
}
</code></pre><p><strong>Use when:</strong></p><ul><li><p>Simple features</p></li><li><p>Don&#8217;t want NgRx overhead</p></li><li><p>Familiar RxJS patterns</p></li></ul><p><strong>Pick one per feature.</strong> Don&#8217;t mix approaches within a single feature.</p><h2>Services</h2><p>Feature-specific services stay private.</p><pre><code>// services/user-api.service.ts
import { Injectable } from &#8216;@angular/core&#8217;;
import { HttpClient } from &#8216;@angular/common/http&#8217;;
import { Observable } from &#8216;rxjs&#8217;;

import { User } from &#8216;../models/user.interface&#8217;;

@Injectable({ providedIn: &#8216;root&#8217; })
export class UserApiService {
  private apiUrl = &#8216;/api/users&#8217;;
  
  constructor(private http: HttpClient) {}
  
  getUsers(): Observable&lt;User[]&gt; {
    return this.http.get&lt;User[]&gt;(this.apiUrl);
  }
  
  getUserById(id: string): Observable&lt;User&gt; {
    return this.http.get&lt;User&gt;(`${this.apiUrl}/${id}`);
  }
  
  deleteUser(id: string): Observable&lt;void&gt; {
    return this.http.delete&lt;void&gt;(`${this.apiUrl}/${id}`);
  }
}
</code></pre><p><strong>Not exported.</strong> Only the store/state service uses this.</p><h2>Public API (index.ts)</h2><p>This is your feature&#8217;s contract with the outside world.</p><pre><code>// src/app/features/user-management/index.ts
// Only export what pages need
export * from &#8216;./user-management.module&#8217;;
export * from &#8216;./user-management.facade&#8217;;

// Everything else stays private:
// &#10060; UserStoreFacade
// &#10060; UserApiService  
// &#10060; UserCardComponent
// &#10060; Store internals (actions, reducers, effects)
// &#10060; Models (unless other features need them)</code></pre><p><strong>Golden rule:</strong> Export as little as possible. Pages should only see:</p><ol><li><p>The module (to import)</p></li><li><p>The facade (for programmatic actions)</p></li><li><p>Maybe types if shared across features</p></li></ol><p>That&#8217;s it.</p><h2>The Module</h2><pre><code>// user-management.module.ts
import { NgModule } from &#8216;@angular/core&#8217;;
import { CommonModule } from &#8216;@angular/common&#8217;;
import { EffectsModule } from &#8216;@ngrx/effects&#8217;;
import { StoreModule } from &#8216;@ngrx/store&#8217;;

import { UserCardComponent } from &#8216;./components/user-card/user-card.component&#8217;;
import { UserManagementContainer } from &#8216;./containers/user-management-container/user-management.container&#8217;;
import { UserManagementFacade } from &#8216;./user-management.facade&#8217;;
import { UserEffects } from &#8216;./store/effects&#8217;;
import { featureKey, reducer } from &#8216;./store/reducers&#8217;;

@NgModule({
  imports: [
    CommonModule,
    EffectsModule.forFeature([UserEffects]),
    StoreModule.forFeature(featureKey, reducer)
  ],
  declarations: [
    UserCardComponent,
    UserManagementContainer
  ],
  exports: [
    UserManagementContainer  // Only export the container
  ],
  providers: [
    UserManagementFacade
  ]
})
export class UserManagementModule { }</code></pre><p><strong>Only export the container.</strong> Internal components stay private.</p><h2>Feature Modes</h2><p>Features should adapt to different contexts via inputs, not configuration.</p><pre><code>@Component({
  selector: &#8216;app-user-management-container&#8217;,
  template: `...`
})
export class UserManagementContainer {
  @Input() mode: &#8216;full&#8217; | &#8216;compact&#8217; | &#8216;selection&#8217; = &#8216;full&#8217;;
  @Input() maxItems?: number;
  @Input() showActions = true;
  
  @Output() userSelected = new EventEmitter&lt;User&gt;();
}</code></pre><p><strong>Usage on different pages:</strong></p><pre><code>&lt;!-- Dashboard: compact view --&gt;
&lt;app-user-management-container 
  mode=&#8221;compact&#8221; 
  [maxItems]=&#8221;5&#8221;&gt;
&lt;/app-user-management-container&gt;

&lt;!-- Admin: full view --&gt;
&lt;app-user-management-container 
  mode=&#8221;full&#8221;&gt;
&lt;/app-user-management-container&gt;

&lt;!-- Selection dialog --&gt;
&lt;app-user-management-container 
  mode=&#8221;selection&#8221;
  (userSelected)=&#8221;handleSelection($event)&#8221;&gt;
&lt;/app-user-management-container&gt;</code></pre><p>Same feature, different contexts. No code changes needed.</p><h2>Common Mistakes</h2><h3>Mistake 1: Exporting Too Much</h3><pre><code>// &#10060; DON&#8217;T
export * from &#8216;./components/user-card/user-card.component&#8217;;
export * from &#8216;./services/user-api.service&#8217;;
export * from &#8216;./store/actions&#8217;;
</code></pre><p>These couple of pages to your internals. Change implementation = break pages.</p><pre><code>// &#9989; DO
export * from &#8216;./user-management.module&#8217;;
export * from &#8216;./user-management.facade&#8217;;
</code></pre><h3>Mistake 2: Smart Components</h3><pre><code>// &#10060; DON&#8217;T: Component injecting services
export class UserCardComponent {
  constructor(private userService: UserApiService) {}
}</code></pre><p>Components should be dumb. Move logic to the container.</p><pre><code>// &#9989; DO: Dumb component with inputs/outputs
export class UserCardComponent {
  @Input() user: User;
  @Output() delete = new EventEmitter&lt;User&gt;();
}</code></pre><h3>Mistake 3: Feature-to-Feature Dependencies</h3><pre><code>// &#10060; DON&#8217;T: Feature importing another feature
import { ProductCatalogFacade } from &#8216;../product-catalog&#8217;;</code></pre><p>Features should be independent. If they need to communicate, let the page coordinate.</p><h3>Mistake 4: No Facade</h3><pre><code>// &#10060; DON&#8217;T: Exporting store directly
export * from &#8216;./store/facade&#8217;;</code></pre><p>Always wrap internals in a feature facade. Gives you flexibility to change implementation.</p><h2>Testing Strategy</h2><h3>Test Components in Isolation</h3><pre><code>describe(&#8217;UserCardComponent&#8217;, () =&gt; {
  it(&#8217;should emit select event on click&#8217;, () =&gt; {
    const component = new UserCardComponent();
    const user = { id: &#8216;1&#8217;, name: &#8216;John&#8217; };
    component.user = user;
    
    spyOn(component.select, &#8216;emit&#8217;);
    component.onSelect();
    
    expect(component.select.emit).toHaveBeenCalledWith(user);
  });
});
</code></pre><h3>Test Containers with Mocked Facade</h3><pre><code>describe(&#8217;UserManagementContainer&#8217;, () =&gt; {
  let facade: jasmine.SpyObj&lt;UserStoreFacade&gt;;
  
  beforeEach(() =&gt; {
    facade = jasmine.createSpyObj(&#8217;UserStoreFacade&#8217;, [&#8217;loadUsers&#8217;]);
  });
  
  it(&#8217;should load users on init&#8217;, () =&gt; {
    const container = new UserManagementContainer(facade);
    container.ngOnInit();
    
    expect(facade.loadUsers).toHaveBeenCalled();
  });
});</code></pre><h3>Test Feature Integration</h3><pre><code>describe(&#8217;UserManagementModule&#8217;, () =&gt; {
  it(&#8217;should render container with users&#8217;, async () =&gt; {
    const fixture = TestBed.createComponent(UserManagementContainer);
    // Test full feature behavior
  });
});</code></pre><p>Clean architecture = easy testing.</p><h2>The Feature Checklist</h2><p>Before marking a feature &#8220;done&#8221;:</p><ul><li><p>Containers handle smart logic, components stay dumb</p></li><li><p>Internal store facade wraps state management</p></li><li><p>External feature facade provides a simple API</p></li><li><p>Only the module and facade are exported via index.ts</p></li><li><p>Services are private to the feature</p></li><li><p>No dependencies on other features</p></li><li><p>Container supports different modes via inputs</p></li><li><p>All components have unit tests</p></li><li><p>Feature works in isolation</p></li></ul><h2>Summary</h2><p>Features are mini-applications. Get the internal structure right, and everything else becomes easy.</p><p><strong>Key takeaways:</strong></p><p>&#9989; Use container/component pattern (smart/dumb)<br>&#9989; Two facades: internal (store) + external (feature)<br>&#9989; Pick one state approach per feature (NgRx/signals/service)<br>&#9989; Keep services private<br>&#9989; Export minimal API via index.ts<br>&#9989; Support multiple modes via inputs<br>&#9989; Test components and containers separately</p><p>Build features this way, and they&#8217;ll work beautifully on any page you compose them into.</p><h2>What&#8217;s Next</h2><p><strong><a href="https://www.better-frontend.dev/pfc-4">PFC #4 - From Monolith to Composition</a></strong><br>Practical migration strategies. How to adopt PFC in existing projects without rewriting everything. Team workflows and common pitfalls.</p><p><strong><a href="https://www.better-frontend.dev/pfc-5">PFC #5 - Real-World Examples</a></strong><br>Complete feature and page implementations working together. Dashboard example, form flows, and handling complex scenarios.</p><h2>Your Challenge</h2><p>Pick one feature from your current codebase:</p><ol><li><p>Can you identify what should be containers vs components?</p></li><li><p>What would you expose via the facade?</p></li><li><p>What services should stay private?</p></li><li><p>How many &#8220;modes&#8221; does it need to support?</p></li></ol><p>Share your analysis in the comments. Next week, we tackle migration strategies.</p><div><hr></div><p><em>Part 3 of the Page-Feature Composition series. Follow along at <a href="https://better-frontend.dev/">better-frontend.dev</a> for practical Angular architecture.</em></p><p><strong>Previous:</strong> <a href="https://better-frontend.dev/pfc-2">PFC #2 - Pages as Orchestrators</a><br><strong>Next:</strong> <a href="https://www.better-frontend.dev/p/pfc-4">PFC #4 - Migration Strategies</a></p><p></p><p><strong>This concludes the Page-Feature Composition series. Follow<a href="https://better-frontend.dev/"> better-frontend.dev</a> for more practical Angular architecture insights.</strong><br><br><strong>The Series:</strong><br>- PFC #1 - <a href="https://better-frontend.dev/pfc-1">The Philosophy</a><br>- PFC #2 - <a href="https://better-frontend.dev/pfc-2">Pages as Orchestrators</a> <br>- PFC #3 - <a href="https://better-frontend.dev/pfc-3">Building Features</a> &#8592; You are here<br>- PFC #4 - <a href="https://better-frontend.dev/pfc-4">Migration Strategies</a><br>- PFC #5 - <a href="https://better-frontend.dev/pfc-5">Real-World Examples</a></p>]]></content:encoded></item><item><title><![CDATA[PFC #2 - Page-Feature Composition: Pages as Orchestrators]]></title><description><![CDATA[How to compose features without losing your mind]]></description><link>https://www.better-frontend.dev/p/pfc-2</link><guid isPermaLink="false">https://www.better-frontend.dev/p/pfc-2</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Thu, 13 Nov 2025 12:56:04 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e193c4b1-eea8-4b3b-849d-8b2f064e69e0_2560x1709.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>TL;DR</h2><p>Pages are conductors, not implementers. They compose features into user experiences through simple templates, handle navigation and routing, and manage feature-to-feature communication when needed. Keep pages under 50 lines of TypeScript by letting features own all business logic.</p><div><hr></div><p>In <a href="https://better-frontend.dev/pfc-1">PFC #1</a>, we introduced the core philosophy: separate features (business logic) from pages (orchestration). Now, let&#8217;s dive deep into how pages actually work as orchestrators.</p><p>Remember that messy 200-line page component mixing users, products, and orders? By the end of this post, you&#8217;ll know exactly how to break it down into a clean, composable architecture.</p><blockquote><p><em><strong>Series Context: This is Part 2 of the Page-Feature Composition series. New here? Start with <a href="https://better-frontend.dev/pfc-1">PFC #1</a> to understand the foundation.</strong></em></p></blockquote><h2>The Conductor Mindset</h2><p>Think about a symphony orchestra. The conductor doesn&#8217;t play violin, trumpet, or drums. They coordinate musicians to create a cohesive performance.</p><p>Pages work the same way. They don&#8217;t implement user management, product catalogs, or order processing. They coordinate features to create user experiences.</p><h3>What Pages Should Do</h3><p>&#9989; Compose features - Arrange feature containers on the screen<br>&#9989; Handle navigation - Respond to route changes and manage routing<br>&#9989; Coordinate communication - Pass data between features when needed<br>&#9989; Apply layout - Structure the page visually<br>&#9989; Manage route-specific state - Track active tab, scroll position, etc.</p><h3>What Pages Should NOT Do</h3><p>&#10060; Business logic - That belongs in features<br>&#10060; API calls - Features handle their own data<br>&#10060; Complex state management - Features manage their own state<br>&#10060; Component implementation - Use feature containers, don&#8217;t build inline</p><p><strong>Golden rule:</strong> If your page component has more than 50 lines of TypeScript, something probably belongs in a feature.</p><h2>Page Anatomy</h2><p>A typical page is mostly template, minimal code:</p><pre><code>// src/app/pages/dashboard/dashboard.page.ts
@Component({
  templateUrl: &#8216;./dashboard.html&#8217;,
  styleUrls: [&#8217;./dashboard.scss&#8217;]
})
export class DashboardPage {
  constructor(private router: Router) {}
  
  navigateToUserDetail(userId: string): void {
    this.router.navigate([&#8217;/users&#8217;, userId]);
  }
}</code></pre><p>That&#8217;s eight lines. The template does the heavy lifting:</p><pre><code>&lt;!-- src/app/pages/dashboard/dashboard.html --&gt;
&lt;div class=&#8221;dashboard-grid&#8221;&gt;
  &lt;section class=&#8221;users-section&#8221;&gt;
    &lt;h2&gt;Team Overview&lt;/h2&gt;
    &lt;app-user-management-container 
      mode=&#8221;compact&#8221;
      [maxItems]=&#8221;8&#8221;&gt;
    &lt;/app-user-management-container&gt;
  &lt;/section&gt;
  
  &lt;section class=&#8221;products-section&#8221;&gt;
    &lt;h2&gt;Latest Products&lt;/h2&gt;
    &lt;app-product-catalog-container 
      mode=&#8221;summary&#8221;&gt;
    &lt;/app-product-catalog-container&gt;
  &lt;/section&gt;
&lt;/div&gt;</code></pre><p>The template is declarative. It says, &#8220;Put user management here, products there.&#8221; That&#8217;s orchestration.</p><h2>Composition Patterns</h2><p>Let&#8217;s explore the most common patterns for composing features.</p><h3>Pattern 1: Grid Layout</h3><p>Multiple features arranged in a grid. The most common pattern.</p><pre><code>&lt;div class=&#8221;grid-layout&#8221;&gt;
  &lt;div class=&#8221;grid-item span-2&#8221;&gt;
    &lt;app-user-management-container mode=&#8221;compact&#8221;&gt;&lt;/app-user-management-container&gt;
  &lt;/div&gt;
  
  &lt;div class=&#8221;grid-item&#8221;&gt;
    &lt;app-notifications-container&gt;&lt;/app-notifications-container&gt;
  &lt;/div&gt;
  
  &lt;div class=&#8221;grid-item span-3&#8221;&gt;
    &lt;app-analytics-container&gt;&lt;/app-analytics-container&gt;
  &lt;/div&gt;
&lt;/div&gt;</code></pre><p>Grid styling in CSS. Features just get composed.</p><h3>Pattern 2: Tabs</h3><p>Different features on different tabs. Page manages which tab is active.</p><pre><code>export class AdminPage {
  activeTab = &#8216;users&#8217;;
}</code></pre><pre><code>&lt;nav class=&#8221;tabs&#8221;&gt;
  &lt;button (click)=&#8221;activeTab = &#8216;users&#8217;&#8221;&gt;Users&lt;/button&gt;
  &lt;button (click)=&#8221;activeTab = &#8216;products&#8217;&#8221;&gt;Products&lt;/button&gt;
&lt;/nav&gt;

&lt;app-user-management-container *ngIf=&#8221;activeTab === &#8216;users&#8217;&#8221;&gt;&lt;/app-user-management-container&gt;
&lt;app-product-catalog-container *ngIf=&#8221;activeTab === &#8216;products&#8217;&#8221;&gt;&lt;/app-product-catalog-container&gt;</code></pre><p>Tab state lives on the page. Features don&#8217;t know about tabs.</p><h3>Pattern 3: Master-Detail</h3><p>One feature drives what another displays.</p><pre><code>export class CustomerPortalPage {
  selectedUserId: string | null = null;
}</code></pre><pre><code>&lt;div class=&#8221;master-detail&#8221;&gt;
  &lt;aside class=&#8221;master&#8221;&gt;
    &lt;app-user-list-container 
      (userSelected)=&#8221;selectedUserId = $event&#8221;&gt;
    &lt;/app-user-list-container&gt;
  &lt;/aside&gt;
  
  &lt;main class=&#8221;detail&#8221;&gt;
    &lt;app-user-detail-container 
      *ngIf=&#8221;selectedUserId&#8221;
      [userId]=&#8221;selectedUserId&#8221;&gt;
    &lt;/app-user-detail-container&gt;
  &lt;/main&gt;
&lt;/div&gt;</code></pre><p>User list emits selection. Page coordinates are passed to the detail view.</p><h3>Pattern 4: Multi-Step Flow</h3><p>A wizard with different features at each step.</p><pre><code>export class OnboardingPage {
  currentStep = 1;
}</code></pre><pre><code>&lt;div class=&#8221;steps&#8221;&gt;Step {{currentStep}} of 3&lt;/div&gt;

&lt;app-profile-form-container 
  *ngIf=&#8221;currentStep === 1&#8221;
  (completed)=&#8221;currentStep = 2&#8221;&gt;
&lt;/app-profile-form-container&gt;

&lt;app-preferences-container 
  *ngIf=&#8221;currentStep === 2&#8221;
  (completed)=&#8221;currentStep = 3&#8221;&gt;
&lt;/app-preferences-container&gt;</code></pre><p>Step management is a page concern. Each step is a feature.</p><h2>Feature Communication</h2><p>Sometimes, features need to communicate with each other. The page coordinates.</p><h3>Simple: Through the Page</h3><pre><code>export class SalesDashboardPage {
  selectedDateRange: DateRange | null = null;
}</code></pre><pre><code>&lt;app-date-picker-container 
  (dateRangeChanged)=&#8221;selectedDateRange = $event&#8221;&gt;
&lt;/app-date-picker-container&gt;

&lt;app-sales-chart-container 
  [dateRange]=&#8221;selectedDateRange&#8221;&gt;
&lt;/app-sales-chart-container&gt;
</code></pre><p>Date picker emits. Page passes to the chart. Simple and explicit.</p><h3>Advanced: Using Facades</h3><p>When you need to trigger actions programmatically:</p><pre><code>export class ProductManagementPage implements OnInit {
  constructor(
    private route: ActivatedRoute,
    private productFacade: ProductCatalogFacade
  ) {}
  
  ngOnInit(): void {
    this.route.params.subscribe(params =&gt; {
      if (params[&#8217;category&#8217;]) {
        this.productFacade.filterByCategory(params[&#8217;category&#8217;]);
      }
    });
  }
}</code></pre><p>Page responds to routes and triggers feature actions via the facade. Container still handles the UI.</p><h2>Common Mistakes</h2><h3>Mistake 1: Business Logic in Pages</h3><p><strong>Don&#8217;t do this:</strong></p><pre><code>export class DashboardPage {
  users: User[] = [];
  
  ngOnInit() {
    this.userService.getUsers().subscribe(users =&gt; {
      this.users = users.filter(u =&gt; u.active);
    });
  }
}</code></pre><p><strong>Do this instead:</strong></p><pre><code>export class DashboardPage {
  // Just compose features in template
}</code></pre><p>Let the user-management feature handle users.</p><h3>Mistake 2: Tightly Coupled Features</h3><p><strong>Don&#8217;t do this:</strong></p><pre><code>&lt;app-user-list [detailComponentRef]=&#8221;userDetail&#8221;&gt;&lt;/app-user-list&gt;
&lt;app-user-detail #userDetail&gt;&lt;/app-user-detail&gt;</code></pre><p>Features shouldn&#8217;t know about each other.</p><p><strong>Do this instead:</strong></p><pre><code>&lt;app-user-list (userSelected)=&#8221;selectedId = $event&#8221;&gt;&lt;/app-user-list&gt;
&lt;app-user-detail [userId]=&#8221;selectedId&#8221;&gt;&lt;/app-user-detail&gt;</code></pre><p>Page coordinates the connection.</p><h3>Mistake 3: Complex Page State</h3><p><strong>Don&#8217;t do this:</strong></p><pre><code>export class DashboardPage {
  userFilters = { ... };
  productFilters = { ... };
  sortOptions = { ... };
  // 50 more properties
}</code></pre><p><strong>Do this instead:</strong></p><pre><code>export class DashboardPage {
  activeTab = &#8216;overview&#8217;; // Minimal page state only
}</code></pre><p>Filters and sort belong in features, not pages.</p><h3>Real Example</h3><p>Here&#8217;s what a real admin dashboard looks like with PFC:</p><pre><code>// 15 lines total
export class AdminDashboardPage {
  constructor(private router: Router) {}
  
  onUserSelected(userId: string): void {
    this.router.navigate([&#8217;/users&#8217;, userId]);
  }
}</code></pre><pre><code>&lt;!-- Template composes 4 features --&gt;
&lt;div class=&#8221;dashboard-grid&#8221;&gt;
  &lt;section class=&#8221;stats&#8221;&gt;
    &lt;app-analytics-container mode=&#8221;summary&#8221;&gt;&lt;/app-analytics-container&gt;
  &lt;/section&gt;
  
  &lt;section class=&#8221;users&#8221;&gt;
    &lt;h2&gt;Recent Users&lt;/h2&gt;
    &lt;app-user-management-container 
      mode=&#8221;compact&#8221;
      (userSelected)=&#8221;onUserSelected($event)&#8221;&gt;
    &lt;/app-user-management-container&gt;
  &lt;/section&gt;
  
  &lt;section class=&#8221;products&#8221;&gt;
    &lt;h2&gt;Top Products&lt;/h2&gt;
    &lt;app-product-catalog-container mode=&#8221;summary&#8221;&gt;&lt;/app-product-catalog-container&gt;
  &lt;/section&gt;
  
  &lt;section class=&#8221;activity&#8221;&gt;
    &lt;app-activity-feed-container&gt;&lt;/app-activity-feed-container&gt;
  &lt;/section&gt;
&lt;/div&gt;</code></pre><p><strong>Total TypeScript:</strong> 15 lines<br><strong>Features composed:</strong> 4 (analytics, users, products, activity)<br><strong>Business logic in page:</strong> 0</p><p>Clean, maintainable, testable.</p><h3>The Page Checklist</h3><p>Before you mark a page &#8220;done,&#8221; ask yourself:</p><ul><li><p>Is the TypeScript file under 50 lines?</p></li><li><p>Does it contain zero business logic?</p></li><li><p>Are all API calls in features, not the page?</p></li><li><p>Is state management delegated to features?</p></li><li><p>Are feature containers doing the heavy lifting?</p></li><li><p>Is the template declarative and easy to scan?</p></li></ul><p>If you answered &#8220;no&#8221; to any of these, refactor something into a feature.</p><h2>Summary</h2><p>Pages as orchestrators are the foundation of PFC. Keep them lean and focused on composition.</p><p><strong>Key takeaways:</strong></p><p>&#9989; Pages compose features via templates<br>&#9989; Keep TypeScript under 50 lines<br>&#9989; Handle navigation and routing only<br>&#9989; Coordinate feature communication when needed<br>&#9989; Let features own all business logic<br>&#9989; Use composition patterns (grid, tabs, master-detail, steps)</p><p>The beauty of this approach? Six months from now, you&#8217;ll open a page file and instantly understand what it does. No archaeological dig required.</p><h2>What&#8217;s Next</h2><p><strong><a href="https://www.better-frontend.dev/pfc-3">PFC #3 - Building Bulletproof Features</a></strong><br>Internal feature architecture. Containers vs components, the facade pattern, state management, and what to export vs keep private.</p><p><strong><a href="https://www.better-frontend.dev/pfc-4">PFC #4 - From Monolith to Composition</a></strong><br>Practical migration strategies. How to adopt PFC in existing projects without rewriting everything. Team workflows and common pitfalls.</p><p><strong><a href="https://www.better-frontend.dev/pfc-5">PFC #5 - Real-World Examples</a></strong><br>Complete feature and page implementations working together. Dashboard example, form flows, and handling complex scenarios.</p><h2>Your Challenge</h2><p>Look at one of your current page components:</p><ol><li><p>How many lines of TypeScript does it have?</p></li><li><p>How much of that could move into features?</p></li><li><p>Which composition pattern would work best?</p></li></ol><p>Share your findings in the comments. Next week, we&#8217;ll build features the right way.</p><div><hr></div><p><em>Part 2 of the Page-Feature Composition series. Follow along at <a href="https://better-frontend.dev/">better-frontend.dev</a> for practical Angular architecture.</em></p><p><strong>Previous:</strong> <a href="https://better-frontend.dev/pfc-1">PFC #1 - The Philosophy</a><br><strong>Next:</strong> <a href="https://www.better-frontend.dev/p/pfc-3">PFC #3 - Building Features </a></p><p><strong>This concludes the Page-Feature Composition series. Follow<a href="https://better-frontend.dev/"> better-frontend.dev</a> for more practical Angular architecture insights.</strong><br><br><strong>The Series:</strong><br>- PFC #1 - <a href="https://better-frontend.dev/pfc-1">The Philosophy</a><br>- PFC #2 - <a href="https://better-frontend.dev/pfc-2">Pages as Orchestrators</a> &#8592; You are here<br>- PFC #3 - <a href="https://better-frontend.dev/pfc-3">Building Features</a><br>- PFC #4 - <a href="https://better-frontend.dev/pfc-4">Migration Strategies</a><br>- PFC #5 - <a href="https://better-frontend.dev/pfc-5">Real-World Examples</a></p>]]></content:encoded></item><item><title><![CDATA[PFC #1 - Page-Feature Composition: The Frontend Philosophy That Actually Works]]></title><description><![CDATA[Why Angular projects turn into spaghetti and how to fix it]]></description><link>https://www.better-frontend.dev/p/pfc-1</link><guid isPermaLink="false">https://www.better-frontend.dev/p/pfc-1</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Thu, 13 Nov 2025 10:37:44 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2ff55abf-023f-43ca-8f45-c6bc22dc2b37_5184x3888.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>PFC #1 - Page-Feature Composition: The Frontend Philosophy That Actually Works</h1><p><em>Why Angular projects turn into spaghetti and how to fix it</em></p><div><hr></div><h2>TL;DR</h2><p>Traditional Angular projects tend to become spaghetti-like as they grow. Components get tightly coupled to contexts. Copy-paste becomes the norm. PFC fixes this by separating <strong>Features</strong> (business logic) from <strong>Pages</strong> (orchestration). Result: truly reusable features, cleaner code, happier teams. While this series focuses on Angular, the core concepts apply to React, Vue, and any other component-based framework.</p><div><hr></div><p>Six months ago, your Angular project was a thing of beauty. Clean components, organized services, everything in its place. Today? You&#8217;re afraid to touch anything because changing one component might break three others you didn&#8217;t know existed.</p><p>Sound familiar? You&#8217;re not alone. Every Angular developer has been there.</p><p>If that describes your current situation, you&#8217;re about to meet your new best friend: <strong>Page-Feature Composition</strong>. It&#8217;s not rocket science, it&#8217;s not revolutionary. It&#8217;s just a sensible way to organize frontend apps that actually scales.</p><blockquote><p><em><strong>Note: This series uses Angular and TypeScript for examples, but the architecture patterns work with React, Vue, Svelte, or any component-based framework. The principles are framework-agnostic; the syntax is Angular.</strong></em></p></blockquote><h2>The Philosophy</h2><p>Stop fighting Angular and start working with it. The key insight is simple: stop asking &#8220;How do I make this component reusable everywhere?&#8221; and start asking &#8220;How do I make features composable on pages?&#8221;</p><h3>The Problem We Keep Creating</h3><pre><code>// We&#8217;ve all written this
@Component({
  selector: &#8216;app-user-card&#8217;,
  template: `...`
})
export class UserCardComponent {
  @Input() user: User;
  
  constructor(
    private userService: UserService,
    private router: Router
  ) {}
  
  editUser() {
    this.router.navigate([&#8217;/admin/users/edit&#8217;, this.user.id]);
  }
}</code></pre><p>This looks innocent enough. Until six months later, when you need the same card in a customer dashboard, but it navigates to the wrong route. Now you&#8217;re adding props to control routes, actions, and styling.</p><p>Welcome to prop hell. Population: your entire codebase.</p><h3>The Mental Shift</h3><p>The difference? Components try to be generic (and fail). Features are specific but composable. Pages orchestrate features into different contexts without modifying them.</p><h2>Folder Structure</h2><p>Here&#8217;s how PFC organizes an Angular project:</p><pre><code>src/app/
&#9500;&#9472;&#9472; features/              # Business logic lives here
&#9474;   &#9500;&#9472;&#9472; user-management/
&#9474;   &#9500;&#9472;&#9472; product-catalog/
&#9474;   &#9492;&#9472;&#9472; order-processing/
&#9500;&#9472;&#9472; pages/                 # Orchestrators
&#9474;   &#9500;&#9472;&#9472; dashboard/
&#9474;   &#9500;&#9472;&#9472; admin/
&#9474;   &#9492;&#9472;&#9472; customer-portal/
&#9500;&#9472;&#9472; shared/                # Utilities
&#9474;   &#9492;&#9472;&#9472; components/
&#9492;&#9472;&#9472; core/                  # App foundation
    &#9492;&#9472;&#9472; auth/
</code></pre><h3>Features</h3><p>Self-contained business capabilities. Everything related to &#8220;user management&#8221; lives in one folder. Components, services, state management, all of it. <strong>Think of features as mini-applications focused on a single business domain.</strong></p><h3>Pages</h3><p>Conductors that compose features into user experiences but contain zero business logic. Just orchestration. A page arranges features on screen and handles navigation.</p><h3>Shared</h3><p>Truly generic stuff. Button components, pipes, utilities that work anywhere. If it doesn&#8217;t belong to a specific feature and isn&#8217;t page-specific, it should be placed here.</p><h3>Core</h3><p>App-wide concerns. Authentication, layout, configuration. Things that are truly global to your application.</p><h2>Pages as Orchestrators</h2><p>This is the key insight that makes everything fall into place. <strong>Pages don&#8217;t implement features. They compose them</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://images.unsplash.com/photo-1745328599767-715cdef0e42d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxzeW1waG9ueSUyMGNvbmR1Y3RvcnxlbnwwfHx8fDE3NjMwMzQ3Mzl8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1745328599767-715cdef0e42d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxzeW1waG9ueSUyMGNvbmR1Y3RvcnxlbnwwfHx8fDE3NjMwMzQ3Mzl8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1745328599767-715cdef0e42d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxzeW1waG9ueSUyMGNvbmR1Y3RvcnxlbnwwfHx8fDE3NjMwMzQ3Mzl8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1745328599767-715cdef0e42d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxzeW1waG9ueSUyMGNvbmR1Y3RvcnxlbnwwfHx8fDE3NjMwMzQ3Mzl8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1745328599767-715cdef0e42d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxzeW1waG9ueSUyMGNvbmR1Y3RvcnxlbnwwfHx8fDE3NjMwMzQ3Mzl8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1745328599767-715cdef0e42d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxzeW1waG9ueSUyMGNvbmR1Y3RvcnxlbnwwfHx8fDE3NjMwMzQ3Mzl8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="5184" height="3888" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1745328599767-715cdef0e42d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxzeW1waG9ueSUyMGNvbmR1Y3RvcnxlbnwwfHx8fDE3NjMwMzQ3Mzl8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:3888,&quot;width&quot;:5184,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;A conductor directs an orchestra with a baton.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&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="A conductor directs an orchestra with a baton." title="A conductor directs an orchestra with a baton." srcset="https://images.unsplash.com/photo-1745328599767-715cdef0e42d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxzeW1waG9ueSUyMGNvbmR1Y3RvcnxlbnwwfHx8fDE3NjMwMzQ3Mzl8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1745328599767-715cdef0e42d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxzeW1waG9ueSUyMGNvbmR1Y3RvcnxlbnwwfHx8fDE3NjMwMzQ3Mzl8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1745328599767-715cdef0e42d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxzeW1waG9ueSUyMGNvbmR1Y3RvcnxlbnwwfHx8fDE3NjMwMzQ3Mzl8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1745328599767-715cdef0e42d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwzfHxzeW1waG9ueSUyMGNvbmR1Y3RvcnxlbnwwfHx8fDE3NjMwMzQ3Mzl8MA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Photo by <a href="https://unsplash.com/@kazuo513">Kazuo ota</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>Think of a symphony orchestra. The conductor doesn&#8217;t play the instruments. They coordinate the musicians (features) to create a cohesive performance (user experience).</p><h3>What You&#8217;re Probably Doing Now</h3><p>Your page component contains over 200 lines of template code that mixes users, products, orders, and notifications. The TypeScript file contains over 50 properties and methods that handle everything.</p><p><strong>It&#8217;s a kitchen sink.</strong> Nobody wants to touch it because changing one thing might break three others.</p><h3>The PFC Way</h3><pre><code>// src/app/pages/dashboard/dashboard.page.ts
@Component({
  templateUrl: &#8216;./dashboard.html&#8217;
})
export class DashboardPage { }</code></pre><pre><code>&lt;!-- src/app/pages/dashboard/dashboard.html --&gt;
&lt;div class=&#8221;dashboard-grid&#8221;&gt;
  &lt;section class=&#8221;users-section&#8221;&gt;
    &lt;h2&gt;Team Overview&lt;/h2&gt;
    &lt;app-user-management-container&gt;&lt;/app-user-management-container&gt;
  &lt;/section&gt;
  
  &lt;section class=&#8221;products-section&#8221;&gt;
    &lt;h2&gt;Latest Products&lt;/h2&gt;
    &lt;app-product-catalog-container&gt;&lt;/app-product-catalog-container&gt;
  &lt;/section&gt;
&lt;/div&gt;</code></pre><p>The page is almost empty. It just arranges features on the screen and adds page-specific layout.</p><ul><li><p>User logic? Lives in the user-management feature.</p></li><li><p>Product logic? Lives in the product-catalog feature.</p></li><li><p>Dashboard layout? That&#8217;s the page&#8217;s only job.</p></li></ul><p><strong>Clean separation. Clear boundaries. No confusion about where code belongs.</strong></p><h2>Features Architecture</h2><p>Each feature is a mini-application focused on one business domain. This is where the real work happens.</p><h3>Internal Structure</h3><pre><code>features/user-management/
&#9500;&#9472;&#9472; components/            # Dumb UI (user cards, forms)
&#9500;&#9472;&#9472; containers/            # Smart orchestrators
&#9500;&#9472;&#9472; services/              # API calls
&#9500;&#9472;&#9472; store/                 # State management (NgRx/signals)
&#9500;&#9472;&#9472; models/                # TypeScript interfaces
&#9500;&#9472;&#9472; user-management.facade.ts     # Public API
&#9500;&#9472;&#9472; user-management.module.ts
&#9492;&#9472;&#9472; index.ts               # What pages can use</code></pre><p>The magic is in what you <strong>export</strong> vs what stays <strong>internal</strong>.</p><h3>Public vs Private API</h3><pre><code>// src/app/features/user-management/index.ts
export * from &#8216;./user-management.module&#8217;;
export * from &#8216;./user-management.facade&#8217;;

// Everything else stays private:
// - Internal components
// - Services
// - Store details
// - Helper utilities
</code></pre><p>Pages can only access what you explicitly export. This prevents tight coupling and keeps your architecture clean.</p><h3>The Facade Pattern</h3><p>Each feature exposes a facade. It&#8217;s a simple API that pages can use to trigger feature actions programmatically.</p><pre><code>// src/app/features/user-management/user-management.facade.ts
@Injectable()
export class UserManagementFacade {
  readonly isLoading$ = this.storeFacade.isLoading$;
  
  constructor(private storeFacade: UserStoreFacade) {}
  
  loadUsers(): void {
    this.storeFacade.loadUsers();
  }
}</code></pre><p>Most of the time, pages use the container component and never interact with the facade. But it&#8217;s there when you need it (like programmatically refreshing data or responding to route changes).</p><h2>Why This Works</h2><p>After using PFC in production for over a year on real enterprise applications, here&#8217;s what we&#8217;ve learned:</p><h3>For Your Code</h3><ul><li><p><strong>True reusability.</strong> The user-management container works on the dashboard, admin panel, and customer portal. Same code, different contexts. No modifications needed.</p></li><li><p><strong>Clear boundaries.</strong> No more &#8220;where does this file go?&#8221; debates. User stuff goes in <code>features/user-management/</code>. The dashboard page goes in <code>pages/dashboard/</code>. Done.</p></li><li><p><strong>Easy testing.</strong> Test features in isolation with simple input and output values. Test pages as composition exercises. No massive mock setups required.</p></li><li><p><strong>Predictable growth.</strong> Your codebase grows linearly with features, not exponentially. Adding feature 20 doesn&#8217;t make features 1-19 more complex.</p></li></ul><h3>For Your Team</h3><ul><li><p><strong>Parallel development.</strong> Developer A builds the user-management feature. Developer B builds the product-catalog feature. Developer C composes them into pages. Minimal merge conflicts.</p></li><li><p><strong>Fast onboarding.</strong> New developers understand the pattern in 10 minutes. &#8220;Features contain business logic. Pages compose features. That&#8217;s it.&#8221;</p></li><li><p><strong>Scales with team size.</strong> Works great for solo developers (clear structure). Scales perfectly to large teams (clear ownership).</p></li></ul><h3>For Your Sanity</h3><ul><li><p><strong>No prop explosion.</strong> Features handle their own modes and configurations internally. No 15+ optional props trying to make components &#8220;generic.&#8221;</p></li><li><p><strong>No copy-paste hell.</strong> Build the feature once, compose it everywhere. The user-management feature is the same whether it&#8217;s on page 1 or page 10.</p></li><li><p><strong>No modification fear.</strong> Changing a feature doesn&#8217;t randomly break unrelated pages. Clear boundaries mean predictable impact.</p></li></ul><h2>When NOT to Use PFC</h2><p>Let&#8217;s be honest. This isn&#8217;t always the answer:</p><p><strong>Skip it for:</strong></p><ul><li><p>Simple landing pages or marketing sites</p></li><li><p>Prototypes or short-term experiments</p></li><li><p>Solo projects under 10 routes</p></li><li><p>Apps with mostly unique, one-off interfaces</p></li></ul><p><strong>Use it for:</strong></p><ul><li><p>Medium to large applications (10+ routes)</p></li><li><p>Long-term projects that will evolve</p></li><li><p>Teams of 3+ developers</p></li><li><p>Apps with recurring UI patterns</p></li></ul><h2>What&#8217;s Next</h2><p>This post introduced the philosophy and high-level structure. We&#8217;ll dive deeper in upcoming posts:</p><p><strong><a href="https://www.better-frontend.dev/pfc-2">PFC #2 - Pages as Orchestrators</a></strong><br>How to compose features without losing your mind. Real composition patterns, handling feature communication, and layout strategies.</p><p><strong><a href="https://www.better-frontend.dev/pfc-3">PFC #3 - Building Bulletproof Features</a></strong><br>Internal feature architecture. Containers vs components, the facade pattern, state management, and what to export vs keep private.</p><p><strong><a href="https://www.better-frontend.dev/pfc-4">PFC #4 - From Monolith to Composition</a></strong><br>Practical migration strategies. How to adopt PFC in existing projects without rewriting everything. Team workflows and common pitfalls.</p><p><strong><a href="https://www.better-frontend.dev/pfc-5">PFC #5 - Real-World Examples</a></strong><br>Complete feature and page implementations working together. Dashboard example, form flows, and handling complex scenarios.</p><h2>Your Mission</h2><p>Before the next post drops, audit your current Angular project:</p><ol><li><p><strong>Find your most complex page component.</strong> How many lines? How many different business concerns does it handle?</p></li><li><p><strong>Count your component variations.</strong> How many &#8220;Card&#8221; components do you have? UserCard, ProductCard, CustomerCard, EmployeeCard...</p></li><li><p><strong>Identify feature boundaries.</strong> What natural business groupings exist? What chunks of functionality could be self-contained?</p></li></ol><p>Drop a comment and share what you find. Don&#8217;t be shy about the mess. We&#8217;ve all been there, and that&#8217;s precisely where PFC shines brightest.</p><p><strong>Bonus challenge:</strong> Try to identify one feature in your current codebase that could be extracted using PFC principles. What would you put in the feature folder? What would the facade expose?</p><div><hr></div><p><em>This is Part 1 of the Page-Feature Composition (PFC) series. Follow along at <a href="https://better-frontend.dev/">better-frontend.dev</a> for practical Angular wisdom that keeps your projects clean and your coffee breaks peaceful.</em></p><p><strong>Next up:</strong> <a href="https://www.better-frontend.dev/pfc-2">PFC #2 - Pages as Orchestrators</a></p><p></p><p><strong>This concludes the Page-Feature Composition series. Follow<a href="https://better-frontend.dev/"> better-frontend.dev</a> for more practical Angular architecture insights.</strong><br><br><strong>The Series:</strong><br>- PFC #1 - <a href="https://better-frontend.dev/pfc-1">The Philosophy</a> &#8592; You are here<br>- PFC #2 - <a href="https://better-frontend.dev/pfc-2">Pages as Orchestrators</a> <br>- PFC #3 - <a href="https://better-frontend.dev/pfc-3">Building Features</a><br>- PFC #4 - <a href="https://better-frontend.dev/pfc-4">Migration Strategies</a><br>- PFC #5 - <a href="https://better-frontend.dev/pfc-5">Real-World Examples</a></p>]]></content:encoded></item><item><title><![CDATA[BlackDuck Deep Dive: Your Frontend Security Detective]]></title><description><![CDATA[You&#8217;ve likely heard the term &#8220;BlackDuck&#8221; tossed around in security meetings and found yourself wondering, &#8220;What does this do, and why should I care?&#8221; Grab a coffee.]]></description><link>https://www.better-frontend.dev/p/blackduck-deep-dive-your-frontend</link><guid isPermaLink="false">https://www.better-frontend.dev/p/blackduck-deep-dive-your-frontend</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Sun, 01 Jun 2025 15:59:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/3939595a-943a-4ad0-8370-5c53d4e0c93e_4000x3000.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You&#8217;ve likely heard the term &#8220;BlackDuck&#8221; tossed around in security meetings and found yourself wondering, &#8220;What does this do, and why should I care?&#8221; Grab a coffee. We&#8217;re exploring Software Composition Analysis (SCA) and why BlackDuck can truly help React and Next.js developers build more secure applications without losing their minds.</p><h3>What Actually Is BlackDuck?  </h3><p>BlackDuck is your npm package security detective. It&#8217;s SCA software that does three important things when you&#8217;re building that Next.js app:</p><ul><li><p><strong>Identifies everything in your dependency tree</strong>, including those sneaky transitive dependencies you never meant to install but are in node_modules anyway.</p></li><li><p><strong>Finds known vulnerabilities</strong> by scanning across your entire dependency tree (much smarter than npm audit).</p></li><li><p><strong>Tracks license compliance</strong> since that seemingly innocent React component may have a GPL license that requires you to open source your entire commercial application.</p></li></ul><p>Here&#8217;s a reality check: <strong>97% of all applications contain open source software</strong>, with most components coming from npm. That React app you&#8217;re creating? BlackDuck is keeping track of everything.</p><h3>Why This Matters for Frontend Devs  </h3><h5><strong>The npm Ecosystem Reality</strong>  </h5><p>Frontend development is like assembling LEGO blocks from npm. We pull in packages for everything, such as date formatting, UI components, and HTTP requests, often without thinking about what we&#8217;re actually installing. The average React application includes hundreds, sometimes thousands, of dependencies when you consider the entire tree.</p><p>Many JavaScript applications have at least one known vulnerability in their dependencies. This isn&#8217;t made up; real vulnerability research shows that it&#8217;s common across the ecosystem. Most React apps operate with publicly documented security flaws.</p><h5>The Transitive Dependency Problem  </h5><p>Here&#8217;s the scary part: <strong>64% of open source components are transitive dependencies</strong>: libraries that other libraries rely on. You install one component, and suddenly you have fifty new packages in your node_modules that you never actually chose.</p><p>This is where npm ls becomes very helpful. It displays the complete dependency tree, helping you identify which of your direct dependencies introduced the problematic code. But manually tracking vulnerabilities across hundreds of transitive dependencies? That&#8217;s where BlackDuck saves you hours of work.</p><h5>Real Attacks, Real Damage  </h5><p>The event-stream incident (2018): A popular npm package downloaded millions of times each week was compromised with malicious code aimed at stealing cryptocurrency.</p><p>The ua-parser-js attack: A widely used library for parsing user agent strings was hijacked to install cryptocurrency miners and steal passwords.</p><p>These weren&#8217;t hypothetical vulnerabilities in forgotten documentation; they were actual attacks targeting real JavaScript applications with real users. BlackDuck helps identify which of your apps were affected via their dependency chains.</p><h3>How BlackDuck Works with Modern Frontend  </h3><h5>Understanding Your JavaScript Dependencies  </h5><p>BlackDuck doesn&#8217;t just scan your package.json. It thoroughly analyzes your entire dependency tree, including nested dependencies that you may not have installed. When working with React, Next.js, or Vue, BlackDuck identifies components using several methods: scanning package manifests, analyzing bundled JavaScript files, and checking Docker containers if you deploy that way.</p><h5>The Knowledge Base  </h5><p>BlackDuck maintains the KnowledgeBase, a massive database of <strong>over 7.8 million open-source components</strong>, with a particular strength in JavaScript. They track npm packages, their version histories, known vulnerabilities, and license obligations. This database is continuously updated, often identifying vulnerabilities in popular frontend libraries weeks before they are reported in official CVE databases.</p><h5>License Compliance (The Underrated Problem)  </h5><p>Open source doesn&#8217;t mean &#8220;free to use however you want.&#8221; Many JavaScript libraries come with license conditions. Some require attribution notices. Others, such as GPL-licensed packages, may require you to open-source your entire commercial application if you use them.</p><p>BlackDuck tracks over 2,500 different license types and their obligations. This is especially important in frontend development, where bundling and minification can accidentally strip necessary license notices.</p><h3>Practical Implementation for React &amp; Next.js  </h3><h5>Start Simple: Scan Your Existing Projects  </h5><p>You don&#8217;t need to change your workflow immediately. Direct BlackDuck at your frontend project and let it analyze what you&#8217;re currently using. For a typical Next.js application, you&#8217;ll receive a comprehensive report detailing all dependencies, their licenses, and any known security issues.</p><p>Think of it as a health check for your codebase. You may be surprised by what&#8217;s hidden in your dependency tree.</p><h5>Use <code>npm ls</code>  as your Swiss Army Knife.  </h5><p>Before diving into BlackDuck&#8217;s advanced features, get familiar with npm ls. This built-in command shows your exact dependency tree. When BlackDuck points out a vulnerability in a transitive dependency, npm ls helps you trace back to see which of your direct dependencies introduced it, making remediation straightforward.</p><pre><code>npm ls [package-name]</code></pre><p>This command tells you precisely which package to update or replace.</p><h5>Focused Remediation Strategy  </h5><p>Start with direct dependencies first: When BlackDuck flags issues, prioritize packages you directly installed. You have more control over replacing or updating them compared to transitive dependencies.</p><p>Understand the impact of your bundle: Not all dependencies are the same. A vulnerability in a build-time tool, such as Webpack, might be less critical than one in a runtime library shipped to browsers. BlackDuck helps you understand which components actually make it into your final bundle.</p><p>Leverage the JavaScript ecosystem: The npm ecosystem is rich with alternatives. If BlackDuck flags an issue with your date manipulation library, switching to an alternative (such as date-fns, dayjs, or luxon) may be quicker than waiting for a patch.</p><h5>Set Intelligent Policies, Not Panic  </h5><p>Configure BlackDuck to focus on what matters most for frontend applications. High and critical CVSS scores in packages handling user input or authentication require immediate attention. A medium-severity vulnerability in a development dependency that never reaches production can be addressed in the next maintenance cycle.</p><p>BlackDuck also tracks whether vulnerabilities have known exploits in the wild. A theoretical security flaw is different from one that attackers are actively exploiting.</p><h3>Investigation and Resolution  </h3><h5>Where to Research JavaScript Vulnerabilities  </h5><p>When BlackDuck flags a vulnerability in your React application, <strong>start with the package&#8217;s GitHub repository</strong> for security announcements, patch releases, or community discussions. Most JavaScript maintainers respond well to security concerns and provide detailed explanations of the impact.</p><p>The npm security advisory database is another valuable resource for understanding how vulnerabilities affect different usage patterns. Sometimes security issues only appear when you use a library in specific ways, so knowing the attack vector helps you assess your actual risk.</p><h5>Frontend-Specific Remediation  </h5><p><strong>Update dependencies gradually</strong>: Unlike backend services, frontend applications often have complex dependency interactions. Update packages incrementally and test thoroughly, especially for significant version changes.</p><p><strong>Commit your lock files</strong>: Your <code>package-lock.json</code> or <code>yarn.lock</code> files are critical for security. They ensure production builds use the identical versions of dependencies as development. When you update packages to fix vulnerabilities, commit the updated lock files to maintain consistency.</p><p><strong>Test across browsers</strong>: Frontend vulnerabilities can behave differently in different browser environments. When updating packages to address security issues, test your applications across multiple browsers and devices.</p><h3>Integration with Your Existing Workflow  </h3><h5>IDE and Editor Integration  </h5><p>Many development environments support BlackDuck integration directly in VS Code or your preferred IDE. This means you can view vulnerability information for packages directly in your editor without needing to change contexts.</p><p>This integration is beneficial when adding new dependencies. Instead of installing first and scanning later, you receive security information about packages before they are added to your <code>package.json</code>. It&#8217;s like having a security advisor watching over you while you code.</p><h5>Works Alongside Your Current Tools  </h5><p>BlackDuck complements the tools you already use, such as <code>npm audit</code>, Snyk, Renovate, or Dependabot. Each tool has its own strengths, and combining them provides better coverage than relying on any single solution.</p><p>If you&#8217;re already using dependency management automation, BlackDuck offers additional context for suggested updates. Instead of blindly accepting all dependency updates, you can prioritize those addressing security vulnerabilities or license issues.</p><h5>Make It Part of Code Reviews  </h5><p>BlackDuck&#8217;s findings should be included in your code review discussions. If someone adds a new dependency to your React application, the review can encompass its security profile and license implications, in addition to its functionality.</p><p>This normalizes security considerations alongside code quality factors, such as linting and testing. You wouldn&#8217;t ship code with syntax errors or failing tests; you shouldn&#8217;t ship code with known security vulnerabilities when practical alternatives exist.</p><h3>The Practical Reality  </h3><p>Security isn&#8217;t a one-time checkbox. <strong>It&#8217;s about continuous improvement</strong> rather than achieving perfect protection, which doesn&#8217;t exist.</p><p>Focus on understanding your dependency tree. Make informed decisions about acceptable risks for your applications and establish processes to address issues as they arise. The JavaScript ecosystem moves quickly. New vulnerabilities are regularly discovered.</p><p>Instead of trying to stay ahead of every possible issue, <strong>build good practices around dependency management and security scanning</strong>. BlackDuck provides the visibility and information you need to make informed decisions for your frontend applications.</p><p>Next time someone mentions BlackDuck in a meeting, you&#8217;ll know exactly what it does and why it matters. More importantly, you&#8217;ll understand how to use it effectively to build better, more secure applications for your users.</p>]]></content:encoded></item><item><title><![CDATA[The Developer’s Journey to Constant Learning and Curiosity]]></title><description><![CDATA[Discover why embracing constant learning as a developer is essential. It&#8217;s the key to making work more enjoyable. A journey full of curiosity and experimentation awaits.]]></description><link>https://www.better-frontend.dev/p/the-developers-journey-to-constant</link><guid isPermaLink="false">https://www.better-frontend.dev/p/the-developers-journey-to-constant</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Thu, 01 May 2025 15:46:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/56c7a37f-5bc1-4c59-b071-a94b88548928_3443x2160.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let&#8217;s be honest: technology is constantly changing. One minute you&#8217;re up to speed, and the next you hear about the next big thing that everyone seems to know about except you. It&#8217;s easy to feel like we&#8217;re always chasing something new. But what if we saw learning not as a chore but as an opportunity? </p><p>In development, learning doesn&#8217;t have to feel like a burden or just a step toward something else. When we make learning a daily habit, a journey instead of a destination, it adds enjoyment to our work and helps us grow. Let&#8217;s explore how maintaining a curious mindset can turn even repetitive days into something meaningful and exciting.</p><h3>Learning as a Journey, Not a Goal  </h3><p>In the development world, there&#8217;s no finish line, and that&#8217;s part of what makes it great. We never really &#8220;finish&#8221; learning. Each new language, tool, or framework we learn opens up additional possibilities we didn&#8217;t even know existed. The best part? We get to choose where our curiosity leads us next. Instead of feeling pressured to learn everything or master the latest trend, let&#8217;s view it as a journey, with each step revealing new and interesting perspectives.</p><p>There&#8217;s no need to stress over what&#8217;s next or worry about falling behind. Embrace the journey as it comes and be open to discovering how much there is to learn.</p><h3>Injecting Curiosity into the Everyday  </h3><p>Experimenting doesn&#8217;t always mean learning an entirely new stack or completely rebuilding your project. It&#8217;s often about those small, daily moments when curiosity sparks. Maybe you try a new way to organize your CSS, or you test a new VS Code extension to see if it improves your workflow. These little experiments are fun, they&#8217;re low stakes, but they can lead to small wins that make the day feel more productive and engaging.</p><p>Even in routine tasks, you can find simple ways to mix things up. Tinker with your process, and challenge yourself to make it faster, smoother, or just a bit more creative.</p><h3>Stepping Outside Your Tech Comfort Zone  </h3><p>Now let&#8217;s discuss the exciting part: stepping outside our comfort zones. Sure, sticking to what we know feels safe, but it can also make work feel monotonous. If you&#8217;re a frontend developer, why not explore backend concepts? Or if you&#8217;re focused on web development, take a weekend to learn about the AI field.</p><p>Exploring beyond your usual tech stack is refreshing; it makes you a more versatile developer. Additionally, observing how other areas of the tech world operate can inspire new ideas and insights that you wouldn&#8217;t gain by staying in one lane. You may even find inspiration in unexpected places.</p><h3>Making Curiosity a Habit, Not a To-Do  </h3><p>Curiosity grows when we nurture it, and making it a habit is easier than it seems. It doesn&#8217;t require us to sit for hours in &#8220;learning mode.&#8221; It&#8217;s more about finding small moments of curiosity throughout the day. Ask &#8220;why&#8221; when you see something done differently, dive deeper into topics that interest you, and don&#8217;t rush through tutorials to check them off your list.</p><p>These small habits help make learning feel less like a task and more like a natural part of your routine. It&#8217;s about letting your curiosity wander and following wherever it goes.</p><h3>Playing Without Pressure  </h3><p>I understand, sometimes we think that if we&#8217;re learning something, it has to be immediately useful or relevant to the next big project. But the beauty of learning often comes from playing without pressure. Explore that new framework just because it interests you. Try building a little app without any intention of publishing it. Not every experiment requires a clear purpose. This freedom to play unencumbered by expectations is where real growth happens.</p><p>When you let yourself learn without expecting a specific outcome, you open up to greater creativity and a deeper understanding. And if it doesn&#8217;t lead anywhere, that&#8217;s okay. It was still worthwhile because it added to your skills.</p><h3>Exploring Different Worlds (Even Outside of Dev)  </h3><p>Curiosity shouldn&#8217;t just stay within the tech realm. Sometimes the best ideas come from outside our daily tasks. Explore DevOps practices, grasp design principles, or delve into product management. Even if you focus on frontend work, learning about backend architecture or cloud infrastructure can be enlightening.</p><p>Learning across different fields not only makes us better developers; it helps us become more well-rounded thinkers. The broader our perspectives, the more connections we can make. So don&#8217;t hesitate to investigate other areas; you might discover your next big idea.</p><h3>Balancing Trends with Core Knowledge  </h3><p>It&#8217;s easy to get caught up in the excitement of the latest and greatest, but it&#8217;s also essential to revisit and strengthen the basics. Staying curious doesn&#8217;t mean chasing every trend; it&#8217;s about building a solid foundation while keeping an eye on new developments.</p><p>When you have a firm grasp of the fundamentals, experimenting with new tools or frameworks becomes an exciting endeavor instead of an overwhelming one. You understand the core principles, allowing you to approach new things with curiosity rather than pressure.</p><h3>Conclusion</h3><p><strong>The Fun is in the Journey!</strong></p><p>Ultimately, learning isn&#8217;t just a means to stay current; it&#8217;s what makes this field so thrilling. There&#8217;s always something new to explore, something to tweak, and something to understand better. It&#8217;s about keeping things fresh, finding joy in small discoveries, and remembering that the journey itself is what matters.</p><p>So the next time you&#8217;re working with a new tool or trying out a fresh approach, remind yourself: you&#8217;re not doing this to check off a box. You&#8217;re doing it because this journey, with all its twists and turns, transforms development from just a job into a craft we enjoy. Keep learning, keep experimenting, and most importantly, keep having fun.</p>]]></content:encoded></item><item><title><![CDATA[Refinement - The Developer’s Secret Weapon in Scrum’s Framework]]></title><description><![CDATA[Exploring how backlog refinement can empower developers, reduce surprises, and streamline sprints in Scrum.]]></description><link>https://www.better-frontend.dev/p/refinement-the-developers-secret</link><guid isPermaLink="false">https://www.better-frontend.dev/p/refinement-the-developers-secret</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Tue, 01 Apr 2025 15:36:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e0ef7301-ff3d-4dc0-950b-d7c269cb35fd_5472x3648.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Exploring how backlog refinement can empower developers, reduce surprises, and streamline sprints in Scrum.</p><p>When you think about Scrum, you likely picture sprints, stand-ups, and retrospectives. But there&#8217;s one part of the Scrum process that doesn&#8217;t get enough credit: refinement. For many teams, refinement is just a recurring meeting on the calendar to clarify tasks or groom the backlog. It often feels like a checkbox item, something to get through before moving on. But here&#8217;s the truth: for developers, refinement is our secret weapon.</p><p>Why? Because refinement helps us stay ahead of the game. It&#8217;s our chance to identify dependencies, clarify requirements, and ask the questions that will save us trouble later. When done correctly, refinement reduces surprises and last-minute issues, setting us up for smoother sprints. Let&#8217;s explore why refinement deserves more attention and how it can make our lives as developers easier.</p><h3>The Developer&#8217;s Perspective on Refinement  </h3><p>As developers, we&#8217;ve all been there. You&#8217;re halfway through a sprint, working on a task, and suddenly boom! An unexpected requirement or hidden dependency disrupts everything. A task you thought was simple suddenly becomes complicated. This is where refinement can be lifesaving.</p><p><strong>Refinement gives us predictability</strong>. It&#8217;s our chance to understand what&#8217;s coming, see potential blockers, and know exactly what we&#8217;re signing up for. It also lets us take ownership of our work before the sprint even begins. When tasks are refined, everyone is aligned, and the sprint is more likely to run smoothly.</p><h3>Real-World Examples: How Refinement Solves Problems  </h3><p>To see exactly how valuable refinement can be, let&#8217;s look at a few scenarios many of us have faced. I&#8217;m talking about those times when you&#8217;re mid-sprint, stressed, and suddenly find out something important was missed. With refinement, these problems can be caught early, turning potential crises into smooth sailing.</p><h4>Example 1: Preventing Scope Creep  </h4><p><strong>Scenario</strong>: <em>You&#8217;re halfway through implementing a new feature when your product owner mentions, &#8220;Oh, can we add this extra bit of functionality?&#8221; Suddenly, a simple task just doubled in size.</em></p><p><strong>Traditional Approach</strong>: Without refinement, this last-minute change feels like a bombshell. You&#8217;re too far along to pull out, but you don&#8217;t have the time or resources for the shift without derailing the sprint. Panic sets in, the deadline is threatened, and everyone feels stressed.</p><p><strong>With Refinement</strong>: During backlog refinement, this feature is carefully discussed and refined. The team clarifies what&#8217;s expected, asks questions, and considers all angles, including potential additions or future changes. The product owner might mention the &#8220;extra bit&#8221; as a &#8220;nice-to-have,&#8221; allowing you to assess the scope accurately before committing. The result? No surprises mid-sprint; just a well-planned feature you&#8217;re ready to tackle.</p><h4>Example 2: Identifying Dependencies Early  </h4><p><strong>Scenario</strong>: <em>Your task requires data from another team&#8217;s API. You&#8217;re ready to start coding, but the API isn&#8217;t prepared yet. Now you&#8217;re blocked, waiting for their team to finish before you can move on.</em></p><p><strong>Traditional Approach</strong>: Without refinement, this situation doesn&#8217;t come up until it&#8217;s too late. You&#8217;re set to code, but you can&#8217;t proceed without that API, and there&#8217;s no backup plan. Your productivity stalls, and the entire sprint is now at risk.</p><p><strong>With Refinement</strong>: During refinement, this <strong>dependency is identified immediately</strong>. You reach out to the other team in advance to confirm the API delivery timeline or plan a workaround. If there&#8217;s a delay, you can rearrange tasks to keep making progress. The sprint continues smoothly, with minimal delays and fewer blockers.</p><h4>Example 3: Eliminating Ambiguities to Avoid Rework  </h4><p><strong>Scenario</strong>: <em>You&#8217;re implementing a user interface change, then halfway through, it becomes clear that the requirement was vague. The stakeholders now want something slightly different, necessitating a significant shift in what has already been built.</em></p><p><strong>Traditional Approach</strong>: Without refinement, you&#8217;re knee-deep in the work before anyone realizes there&#8217;s a misunderstanding. The requirements were too vague, but there was no chance to clarify. Now, you&#8217;re stuck reworking the code, using time that could&#8217;ve been spent elsewhere.</p><p><strong>With Refinement</strong>: In the refinement session, you ask questions about the new UI, such as &#8220;Does this include feature X? Do you want interaction Y?&#8221; By clarifying the details beforehand, you and the stakeholders share the same understanding from the outset. The task is straightforward, you&#8217;re focused, and rework is minimal or avoided entirely.</p><h3>Why Skipping Refinement Can Haunt You  </h3><p>So what happens when we don&#8217;t prioritize refinement? We&#8217;ve all seen it go wrong:</p><ul><li><p><strong>Last-Minute Scope Bombs</strong>: You&#8217;re mid-sprint and realize a critical requirement was missed. Now you&#8217;re scrambling to fit it in, and it&#8217;s consuming time you hadn&#8217;t planned for.  </p></li><li><p><strong>Dependency Landmines</strong>: Without refinement, tasks with external dependencies can hit blockers just when you&#8217;re in the thick of things, forcing you to wait on another team or service.  </p></li><li><p><strong>Developer Frustration</strong>: Working on tasks with unclear requirements or unknown blockers can lead to burnout. Refinement is our opportunity to address these issues before they arise.  </p></li></ul><p>These problems usually arise when we&#8217;re already mid-sprint and under pressure. This is why, as developers, we need to prioritize refinement, not just for the process, but for our own well-being.</p><h3>Using Refinement as a Tool for Success  </h3><p>Here&#8217;s how we can view refinement as a developer toolkit to avoid mid-sprint headaches:</p><ul><li><p><strong>Spot Dependencies Early</strong>: Part of refinement involves identifying who or what we&#8217;ll need to complete a task. Is there a service dependency? Do we need to coordinate a database change? It&#8217;s best to pinpoint these issues upfront.  </p></li><li><p><strong>Catch Potential Problems</strong>: The earlier we identify tricky aspects of a story, the better. Refinement allows us to voice concerns and bring up edge cases, so we don&#8217;t get blindsided later.  </p></li><li><p><strong>Clarify Requirements</strong>: Refinement is the ideal time to examine any vague requirements and clarify them, thereby preventing future blockers.  </p></li></ul><p>Refinement isn&#8217;t just a routine meeting. It&#8217;s where we make sure we&#8217;re ready for what&#8217;s ahead, turning uncertainties into clear steps forward. Think of it as a chance to prepare for a journey: the more familiar we are with the route, the fewer surprise detours we&#8217;ll encounter.</p><h3>A 3-Step Approach to Streamlined, Effective Refinement  </h3><p>Over time, I&#8217;ve found that one of the most efficient ways to approach refinement is with a simple, 3-step framework. This process has worked well for me, keeping the team engaged and making the sessions much more productive. Here&#8217;s how it works:</p><ol><li><p><strong>Divide and Conquer</strong>: Start by assigning specific tasks to individual team members to read through and understand. This way, each person takes ownership of particular stories, rather than the whole team getting bogged down in every detail. If a task covers both frontend and backend, it&#8217;s best to pair up specialists (UI and backend developers) for a full-stack task. Their job is to dive deep and prepare to share their findings with the team.</p></li><li><p><strong>Analyze and Document</strong>: Each &#8220;owner&#8221; or pair thoroughly reviews the task, discussing dependencies, identifying potential blockers, and listing resources they might need. They should document everything: to-dos, helpful links, dependencies, and any issues they foresee. This pre-work is crucial. It means the rest of the team doesn&#8217;t have to start from scratch in the refinement session.</p></li><li><p><strong>Present Findings in the Refinement Session</strong>: Now it&#8217;s time for the refinement meeting. The task owners present their findings to the team, summarizing key points so that everyone understands the task without needing to conduct a deep dive themselves. This is when other team members can jump in, add insights, or raise flags they notice. It keeps the session focused since the &#8220;heavy lifting&#8221; has already been done, allowing everyone to focus on filling in gaps or discussing solutions.</p></li></ol><p>This 3-step approach has proven to be a game-changer for me. Rather than dragging the entire team through every detail, it keeps things streamlined and efficient. It&#8217;s less draining than going line by line, keeps everyone engaged, and ensures that the team gains real value.</p><h3>Wrapping It Up</h3><p><strong>Refinement as a Developer&#8217;s Superpower</strong>!</p><p>Refinement isn&#8217;t just a Scrum ritual; it&#8217;s a superpower for developers. It&#8217;s our chance to take control, turn uncertainty into clarity, and make sprint life much easier. When we treat refinement as a strategic tool, we&#8217;re setting ourselves and the entire team up for success.</p><p>So, next time a refinement session comes up, think of it as a tool in your kit. Jump in, ask questions, and use it to shape your work before it shapes you. A refined backlog is a developer&#8217;s best friend. Let&#8217;s make the most of it.</p>]]></content:encoded></item><item><title><![CDATA[Why Technology Shifts Are Golden Opportunities - Seize the Chance to Build a Better Product]]></title><description><![CDATA[Technology shifts are more than just technical necessities; they&#8217;re opportunities to rethink, reorganize, and improve your product for lasting success. Here&#8217;s why you shouldn&#8217;t miss this chance.]]></description><link>https://www.better-frontend.dev/p/why-technology-shifts-are-golden</link><guid isPermaLink="false">https://www.better-frontend.dev/p/why-technology-shifts-are-golden</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Sat, 01 Mar 2025 16:30:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0beb35c8-78ba-4cb3-8a50-0de5f150a66a_4093x2729.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Technology is changing rapidly, and every company faces the need to migrate, upgrade, or switch to new platforms. These changes are more than just technical requirements. Technology shifts present a unique opportunity to enhance the product significantly: rethink, reorganize, and position ourselves for long-term success.</p><p>This isn&#8217;t just about moving code from one framework to another. It&#8217;s about leveraging a moment of change to future-proof the product, enhance user experiences, and establish systems that are scalable and easy to maintain over time. Missing this opportunity would be a waste of a chance to build a better product for both our users and our development team.</p><h3>1. A Technology Shift is NOT Just a Simple Move  </h3><p>It&#8217;s easy to view a migration as a simple task: take what we have, move it over, and continue. However, if we think that way, we&#8217;ll miss a significant opportunity to improve our product.</p><p>Migrating isn&#8217;t simply copying and pasting existing code. Each technology offers its own benefits, and if we don&#8217;t take advantage of them, we&#8217;ll trap ourselves in outdated ways. Think of it like moving to a new house. You wouldn&#8217;t just pack up all the clutter and inefficiencies from your old home; you&#8217;d use the move to reorganize and improve your living space.</p><p>A successful migration lets us:</p><ul><li><p>Rethink how our architecture works, applying best practices we&#8217;ve learned over the years.</p></li><li><p>Remove inefficiencies that have developed in our legacy code.</p></li><li><p>Prepare for future growth by designing a system that can adapt and expand as new demands arise.  </p></li></ul><p><strong>A &#8220;lift and shift&#8221; approach keeps old problems alive.</strong> By treating this as a chance to rethink and redesign, we can ensure our product doesn&#8217;t just survive this transition; it thrives beyond it.</p><h3>2. The Perfect Moment to Tackle Technical Debt  </h3><p>Technical debt is a reality for any long-term product development effort. Over time, shortcuts are taken, code becomes messy, and workarounds accumulate. These decisions may have been necessary at the time, but now they create obstacles for future development.</p><p><strong>Migrating to new technology is the best time to address this debt.</strong> Instead of carrying it over to a new platform, we can wipe the slate clean and build a more efficient foundation.</p><p>By using this time to refactor:</p><ul><li><p>Development becomes faster. Clean, well-structured code is easier to maintain and extend, resulting in fewer bugs and shorter development cycles.</p></li><li><p>Scaling becomes smoother. Tackling technical debt now means we&#8217;ll face fewer bottlenecks as we grow, making it easier to manage increased user loads or add new features without disrupting existing systems.  </p></li></ul><p>This is our opportunity to get it right. We shouldn&#8217;t waste this moment by bringing legacy issues along. Cleaning up now will save significant time and resources in the years ahead.</p><h3>3. An Opportunity to Greatly Improve User Experience (UX)  </h3><p>Changing technology is also an ideal time to review and enhance our product&#8217;s user experience (UX). Today&#8217;s users expect seamless, fast, and intuitive interactions, and technology shifts give us the chance to rethink how users engage with the product.</p><p>Why is this so essential? User experience impacts business results. Better UX leads to:</p><ul><li><p>Higher user satisfaction directly affects retention and conversion rates.</p></li><li><p>Fewer support requests, since users can navigate the product without confusion or frustration.</p></li><li><p>Quicker onboarding, as intuitive design helps new users get up to speed faster.  </p></li></ul><p>During this migration, we have the chance to:</p><ul><li><p><strong>Streamline user journeys</strong> by eliminating unnecessary steps, making interactions smoother, and enabling users to achieve their goals more efficiently.</p></li><li><p><strong>Optimize performance</strong>. Better performance means a faster, smoother experience for users. Speed matters; every second saved decreases drop-offs and boosts engagement.</p></li><li><p><strong>Modernize the interface</strong>. Now is the time to apply modern UX principles, making our product not only functional but enjoyable to use.  </p></li></ul><p>This shift is not just a technical necessity; it&#8217;s an opportunity to deliver a better product for users and ultimately enhance business results.</p><h3>4. Building a Strong Foundation for Innovation  </h3><p>A technology migration isn&#8217;t just about solving today&#8217;s issues; it&#8217;s about preparing for the future. Modernizing the system now means a more flexible, modular, and scalable foundation for innovation.</p><ul><li><p><strong>Modularity</strong>: By creating smaller, independent components, we make it easier to evolve parts of the system separately. This prevents new features or changes from disrupting the entire product.  </p></li><li><p><strong>Scalability</strong>: The demands of today won&#8217;t be the same tomorrow. By modernizing now, we prepare the system to grow with user needs, whether that involves handling more data, more users, or more complexity.  </p></li><li><p><strong>Flexibility</strong>: A cleaner architecture allows us to adjust quickly to future technology changes. Whether integrating AI-driven features, adapting to new devices, or connecting with emerging technologies, a flexible system is crucial.  </p></li></ul><p>By making the right choices today, we create an environment that facilitates the implementation of future innovations, resulting in faster time-to-market for new features and a competitive edge in the industry.</p><h3>5. Managing Expectations: This Takes Time, But It&#8217;s Important  </h3><p>We must recognize that a careful migration won&#8217;t happen overnight. Rushing the process risks overlooking the valuable opportunities mentioned earlier. Doing it properly takes time, but it&#8217;s time well invested.</p><p>By carefully managing the migration process:</p><ul><li><p>We prevent new problems that might arise from hasty transitions.</p></li><li><p>We ensure long-term stability. A well-planned migration reduces the risk of costly mistakes later, sparing us from having to redo work.</p></li><li><p>We stay aligned with strategic goals. This isn&#8217;t just a technical shift; it&#8217;s a business transformation. The result will be a stronger, more resilient product that meets both technical and business goals.  </p></li></ul><p>Rushing through these steps would miss an opportunity to build something truly extraordinary.</p><h3>Conclusion</h3><p>Take This Chance! This Migration is an Opportunity to Build Better  </p><p>A technology shift isn&#8217;t just a task on a checklist; it&#8217;s a chance to build better. This migration enables us to rethink our approach, reorganize for optimal performance and maintainability, and enhance the product for both developers and users.</p><p>By using this migration to eliminate technical debt, improve the user experience, and future-proof the product, we are investing in the long-term success of both the application and the business.</p><p>This isn&#8217;t only a technical project; it&#8217;s a strategic move that will yield benefits in speed, scalability, and usability. Let&#8217;s not waste the chance to create something better, both today and in the future.</p>]]></content:encoded></item><item><title><![CDATA[5 Practical Ways to Bring UX into Your Development Workflow (Without a UX Designer)]]></title><description><![CDATA[Even if your team lacks a dedicated UX designer, you can still bring UX principles into your development process with these practical tips.]]></description><link>https://www.better-frontend.dev/p/5-practical-ways-to-bring-ux-into</link><guid isPermaLink="false">https://www.better-frontend.dev/p/5-practical-ways-to-bring-ux-into</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Sat, 01 Feb 2025 16:20:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/09e047f1-7392-4962-b418-7b6e2fd20603_5377x3295.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Even if your team lacks a dedicated UX designer, you can still bring UX principles into your development process with these practical tips.</p><p>As a developer without a UX designer, you might think UX is beyond your reach. The good news is, you don&#8217;t need a whole UX team to make your products more user-friendly. With the right tools and approach, you can easily incorporate UX principles into your workflow and significantly enhance the final product.</p><p>Here are five practical steps to enhance UX without needing a dedicated designer.</p><h3>1. Start with What&#8217;s Already Out There: Use Existing Patterns  </h3><p>Before diving into design work, look at what&#8217;s already working. If your team doesn&#8217;t have a formal design system, that&#8217;s fine! You can still use existing patterns to maintain consistency and user-friendliness.</p><p>Here&#8217;s how:  </p><ul><li><p>Leverage established design systems like Google&#8217;s Material Design or Apple&#8217;s Human Interface Guidelines. These systems offer pre-built patterns that are widely used and easy to implement.  </p></li><li><p>Check your company&#8217;s past work: Even without an official design system, there&#8217;s likely a history of interfaces in your company. Use those to maintain consistency.  </p></li><li><p>Pro Tip: Consistency is helpful. Sticking to familiar patterns saves time and creates a smoother experience for users.</p></li></ul><h3>2. Map Out User Flows with Figma  </h3><p>After identifying the design patterns you want to follow, map out how users will interact with your product. You don&#8217;t need a fancy UX background; just a little empathy and a tool like Figma.</p><p>Steps:  </p><ul><li><p>Use Figma to create simple wireframes. They don&#8217;t have to be perfect; they are just for visualizing the flow.  </p></li><li><p>Add annotations in Figma to explain each step of the process. It&#8217;s like leaving notes for yourself or anyone else involved in the project.  </p></li><li><p>Include screenshots from existing products or websites to highlight interactions you want to replicate. This helps when communicating ideas with stakeholders.  </p></li></ul><p>The key is to ensure the flow makes sense from the user&#8217;s perspective. Ask yourself: Is it clear what to do next? Are the necessary actions easily accessible?</p><h3>3. Collaborate and Gather Feedback in Confluence  </h3><p>Once your wireframes and user flows are laid out, bring the rest of the team in. Confluence is an excellent tool for this. It helps document everything and collect feedback in one place.</p><p>Here&#8217;s how to use Confluence for collaboration:  </p><ul><li><p>Embed your Figma designs into a Confluence page so everyone on your team can view them.  </p></li><li><p>Add clear descriptions and explanations of the user flow and key interactions to ensure a seamless experience. Make it easy for stakeholders to follow along, even if they aren&#8217;t designers.  </p></li><li><p>Use comments: Encourage your team and stakeholders to leave feedback directly in Confluence. This helps keep track of changes and ideas all in one place.  </p></li></ul><p>By combining Figma and Confluence, you create a straightforward process that allows everyone to stay in sync, even if they aren&#8217;t using the design tools.</p><h3>4. Iterate Quickly with Early Feedback  </h3><p>One of the best parts of using tools like Figma is how quickly you can refine designs. Don&#8217;t wait until everything is built to get feedback. Get input early and often from your team and stakeholders.</p><p>Quick tips:  </p><ul><li><p>Share your Figma mockups early to get input before coding begins.  </p></li><li><p>Use the feedback you collect in Confluence to adjust the mockups quickly. This way, you can ensure the design aligns with business goals before serious development starts.  </p></li><li><p>If possible, show the design to users or potential users. Even casual feedback from colleagues can help identify usability issues early.  </p></li></ul><p>This feedback loop saves you from having to rework code later, which can be time-consuming and frustrating. Quick adjustments in Figma are much faster than a complete development overhaul.</p><h3>5. Use UX Decision Records to Build a Knowledge Base  </h3><p>Throughout this process, you&#8217;ll make many choices about how things should work and look. Don&#8217;t let those critical decisions slip away! Creating UX Decision Records (UXDRs) is a straightforward way to track key design choices.</p><p><strong>What to include in a UXDR:</strong>  </p><ul><li><p>The Problem: Describe the issue or need that led to the UX decision.  </p></li><li><p>The Solution: Detail the new flow, pattern, or design that was chosen.  </p></li><li><p>Feedback &amp; Approvals: Capture input from stakeholders and note who approved the design.  </p></li><li><p>Guidelines for Future Use: If you create a reusable pattern (such as handling bulk actions), include instructions on how to use it in future projects.  </p></li></ul><p>Keeping these decision records organized in Confluence or another knowledge base will make future projects easier to manage. You won&#8217;t have to start from scratch every time a similar UX issue arises.</p><h3>Wrapping It Up  </h3><p>Incorporating UX into your web development process doesn&#8217;t have to be complicated or require a dedicated UX designer. By utilizing existing patterns, mapping user flows in Figma, collaborating in Confluence, and maintaining feedback loops, you can create user-friendly products that align with business needs.</p><p>The main takeaway? Iterate quickly and communicate clearly. By involving stakeholders early and documenting your decisions, you can avoid last-minute changes and rework, while also providing a better user experience.</p>]]></content:encoded></item><item><title><![CDATA[Why UX Matters in Web Development - A Guide for Developers]]></title><description><![CDATA[UX is essential in web development, but what does it really involve?]]></description><link>https://www.better-frontend.dev/p/why-ux-matters-in-web-development</link><guid isPermaLink="false">https://www.better-frontend.dev/p/why-ux-matters-in-web-development</guid><dc:creator><![CDATA[Ireneusz Budzowski]]></dc:creator><pubDate>Wed, 01 Jan 2025 16:07:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/4f0b2a0f-35ca-44df-810b-27f4e0df1729_1920x1280.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>UX is essential in web development, but what does it really involve? Discover how UX boosts user satisfaction, speeds up development, and simplifies life for developers, even without a dedicated UX designer.</p><p>When you&#8217;re deep into web development, it&#8217;s easy to concentrate on the code. You have your frameworks, APIs, and deployment pipelines, all running smoothly. However, as many developers find out, if users can&#8217;t navigate your site easily or find it frustrating, no amount of clean code will make a difference. That&#8217;s where user experience (UX) comes in.</p><h3>What Is UX?  </h3><p>User Experience (UX) is how users feel when interacting with your website or app. UX design aims to make products usable, enjoyable, and accessible. It involves understanding users&#8217; needs and designing with empathy to create a product that feels intuitive and easy to navigate.</p><p>A website may work perfectly from a technical standpoint, but if users struggle to find key features or feel overwhelmed by confusing layouts, their experience suffers. The result? They leave, bounce rates increase, and conversions decline. Good UX design helps prevent these problems by guiding users through smooth interactions.</p><h3>Who Is a UX/UI Designer?  </h3><p>You may have heard the terms&#8217; UX designer&#8217; and &#8216;UI designer&#8217; mentioned. While they often work closely together, they have different roles:</p><ul><li><p>UX designers focus on the overall experience, researching user behavior, building user flows, and testing how the product should function.</p></li><li><p>UI designers concentrate on the look and feel, choosing layouts, colors, fonts, buttons, and visual hierarchies to ensure the interface is both visually appealing and functional.</p></li></ul><p>In many companies, these roles may overlap, and one person might handle both UX and UI design. The key is to ensure the product looks good and works well for the user.</p><h3>What If There&#8217;s No UX Expert in Your Company?  </h3><p>Let&#8217;s face it - what if your company doesn&#8217;t have a dedicated UX designer? You might be in a small team, or perhaps the focus has always been on backend performance. Does that mean UX should be overlooked?</p><p>Definitely not! Here are some steps you can take even without a dedicated UX person:</p><ul><li><p>Look for a design system. Companies often have a design framework, even if it is informal. The design system could be a collection of reusable components or internal guidelines. Ask around - there&#8217;s likely something you can use. If not, consider established libraries like Material UI or Bootstrap. These libraries provide pre-made components that adhere to best practices, ensuring consistency.</p></li><li><p>Gather inspiration. If you lack a design system, get creative. Platforms like Dribbble are great for finding design ideas. While not every idea will be a perfect fit, it&#8217;s a good way to explore modern design trends.</p></li><li><p>Put yourself in the user&#8217;s position. Think about your product from a user&#8217;s view. Is it clear what they should do next? Are the key features easily accessible? A bit of user-focused thinking can make a significant difference.</p></li><li><p>Use Figma to manage designs. Tools like Figma allow you to maintain designs effectively, even without a dedicated UX designer. Figma is free to use and lets you import libraries like Material UI or Bootstrap into your project. You can quickly create interfaces by selecting and customizing components, such as buttons, inputs, and navigation items, while adjusting properties like size, color, and spacing. It&#8217;s also a collaborative tool, letting multiple team members share feedback and make adjustments in real-time.</p></li><li><p>Get feedback early. You don&#8217;t need extensive UX research to improve the user experience. Gather input from colleagues, stakeholders, or potential users to identify usability issues early on, before development starts.</p></li></ul><p>UX doesn&#8217;t have to be complicated, and small changes can significantly enhance your project&#8217;s success.</p><h3>Why UX Should Matter to Developers  </h3><p>As developers, it&#8217;s easy to see design as someone else&#8217;s responsibility. However, focusing on UX can actually make your life simpler. Here&#8217;s how:</p><ul><li><p>Fewer changes after development. One of the biggest hassles for developers is last-minute changes. By prioritizing UX early, you can avoid usability issues later in the process. It&#8217;s much easier to tweak designs before coding than to redo everything after deployment.</p></li><li><p>Faster feedback from stakeholders. Good design streamlines feedback. Since making changes to mockups or wireframes is straightforward, you can get quick alignment from stakeholders and avoid major revisions. This keeps the development cycle smooth.</p></li><li><p>Easier styling for developers. Not every developer enjoys adjusting CSS for perfect designs. A well-planned UI design provides a clear path, enabling developers to focus on implementation without getting bogged down in styling issues.</p></li><li><p>Prevents mistakes. Good UX design requires you to think through the user&#8217;s journey in detail before development begins. This minimizes costly errors and confusing user flows that require later fixes.</p></li></ul><h3>How UX Helps Developers Collaborate More Efficiently  </h3><p>Good UX helps users and improves the entire development process. When designs are well thought out and thoroughly documented, developers have a clear understanding of what the final product should look like. This reduces confusion, speeds up development, and ensures the outcome meets everyone&#8217;s expectations.</p><p>A clear UX design acts as a guide, making it easier for developers and minimizing miscommunication between teams.</p><h3>Conclusion  </h3><p>Ultimately, UX isn&#8217;t just for designers; it&#8217;s a vital component of web development that affects everyone, including developers. Even if your team lacks a UX expert, grasping basic UX principles can help you create better, more user-friendly products.</p><p>By focusing on the user, using tools like Figma, and utilizing design systems, you can build websites and applications that not only work well but are also enjoyable for users. So, next time you&#8217;re coding, remember: great code is essential, but great UX is what truly matters.</p>]]></content:encoded></item></channel></rss>