<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Senior Engineering on Peter Fulop</title><link>https://peterfulop.tech/tags/senior-engineering/</link><description>Recent content in Senior Engineering on Peter Fulop</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>Peter Fulop</copyright><lastBuildDate>Sat, 23 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://peterfulop.tech/tags/senior-engineering/index.xml" rel="self" type="application/rss+xml"/><item><title>Effectiveness anti-patterns — part 6: YAGNI: don’t abstract before the second use case</title><link>https://peterfulop.tech/p/yagni-dont-abstract-before-the-second-use-case/</link><pubDate>Sat, 23 May 2026 00:00:00 +0000</pubDate><guid>https://peterfulop.tech/p/yagni-dont-abstract-before-the-second-use-case/</guid><description>&lt;img src="https://peterfulop.tech/p/yagni-dont-abstract-before-the-second-use-case/yagni-dont-abstract-before-the-second-use-case.jpg" alt="Featured image of post Effectiveness anti-patterns — part 6: YAGNI: don’t abstract before the second use case" /&gt;&lt;h2 id="the-prototype-was-meant-to-ask-a-question"&gt;The prototype was meant to ask a question
&lt;/h2&gt;&lt;p&gt;YAGNI — You Aren&amp;rsquo;t Gonna Need It — is usually explained as: do not build something until you actually need it.&lt;/p&gt;
&lt;p&gt;The version of over-engineering I have learned to watch most carefully is not the dramatic one.&lt;/p&gt;
&lt;p&gt;It is the small, reasonable-looking abstraction that appears during a prototype.&lt;/p&gt;
&lt;p&gt;A new piece of work lands. The requirement is small, the user is one team, the use case is specific. The prototype has one job: answer the question on the table.&lt;/p&gt;
&lt;p&gt;Then a second thought appears:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;If this works, others will probably want it too.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That thought is not wrong. It is often a useful instinct.&lt;/p&gt;
&lt;p&gt;The risk is letting it drive the first version.&lt;/p&gt;
&lt;p&gt;Instead of writing the simplest thing that answers the question, the prototype starts turning into a framework. A configuration layer. A second abstraction. Support for variants that nobody has asked for yet.&lt;/p&gt;
&lt;p&gt;The trap with prototypes is that I often know the least about the problem at the exact moment the future shape looks easiest to design.&lt;/p&gt;
&lt;p&gt;The second use case does not exist yet. The third is even less defined. But the blank file makes it easy to imagine a clean framework, all the right extension points, and all the variants that might arrive later.&lt;/p&gt;
&lt;p&gt;That can feel like care. Sometimes it is. Often, in a prototype, it is just over-engineering arriving early.&lt;/p&gt;
&lt;h2 id="a-ci-prototype-we-kept-intentionally-small"&gt;A CI prototype we kept intentionally small
&lt;/h2&gt;&lt;p&gt;A while back, our team wanted to improve how we shipped DAG images to our Cloud Composer environment.&lt;/p&gt;
&lt;p&gt;The pattern was roughly this: a data engineer built the image locally, pushed it to Artifact Registry from their laptop, and then updated the DAG. It worked, in the sense that production kept running. It also broke in small but annoying ways: wrong tag, stale dependency, local build differences, unclear ownership of what had actually been deployed.&lt;/p&gt;
&lt;p&gt;The agreed direction was to move image builds into CI.&lt;/p&gt;
&lt;p&gt;I took the work on. The goal was deliberately narrow: prove that one DAG image, for one representative pipeline, could be built and pushed automatically through GitHub Actions when its source folder changed.&lt;/p&gt;
&lt;p&gt;That was enough for the first question.&lt;/p&gt;
&lt;p&gt;While sketching the workflow, I could already see the reusable version.&lt;/p&gt;
&lt;p&gt;Multiple image conventions. Several target environments. Branch-based tagging. A registry abstraction. A small YAML schema for teams to declare their own build configuration.&lt;/p&gt;
&lt;p&gt;All of that might become useful later.&lt;/p&gt;
&lt;p&gt;But at that point we had one real pipeline, one team, and one deployment path. Turning that into a shared abstraction would have meant designing from guesses instead of evidence.&lt;/p&gt;
&lt;p&gt;In review, we named the trade-off explicitly: are we trying to prove the CI path, or are we designing the future deployment platform?&lt;/p&gt;
&lt;p&gt;That question helped keep the first version honest.&lt;/p&gt;
&lt;p&gt;So I kept the first version intentionally boring: one workflow file, one image, one Artifact Registry path, and one DAG folder. It also worked.&lt;/p&gt;
&lt;p&gt;The next morning someone copied it as a template for a second DAG image. The differences between the two were already enough to show that a shared abstraction would have needed more evidence.&lt;/p&gt;
&lt;p&gt;A few weeks later, another team asked if they could use the same pattern. Their setup had a separate registry, an environment-specific build step, and a security review checkpoint that did not exist in our pipeline. A shared platform designed too early would probably have made those changes harder, not easier.&lt;/p&gt;
&lt;p&gt;What we have now is a small library of similar GitHub Actions workflows, each specific to its team. There is some duplication. There is also no premature framework in the way.&lt;/p&gt;
&lt;p&gt;The day a shared abstraction is actually needed, we will have several concrete examples to abstract from instead of one example and a lot of imagined ones.&lt;/p&gt;
&lt;p&gt;I do not want to overstate it. This was a small, deliberately boring piece of CI.&lt;/p&gt;
&lt;p&gt;But it was a clean moment of restraint. We shipped the narrow version, and let the second real case teach us what the shared shape should be.&lt;/p&gt;
&lt;h2 id="what-i-had-to-admit"&gt;What I had to admit
&lt;/h2&gt;&lt;p&gt;For a long time I called this kind of thinking &amp;ldquo;planning ahead.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Sometimes that was true. Often it was a more comfortable story than the real one: abstraction was more interesting to write than the boring concrete version.&lt;/p&gt;
&lt;p&gt;Some of that came from my early object-oriented training. Open/closed. Dependency inversion. Interfaces before implementations. Those principles are useful in long-lived codebases. They are less useful when a prototype has one user, one use case, and one question to answer.&lt;/p&gt;
&lt;p&gt;Same habit. Different context.&lt;/p&gt;
&lt;p&gt;The simpler version is often harder to choose because it feels less impressive while you are writing it. But it is usually easier for the next person to read, review, debug, and change.&lt;/p&gt;
&lt;p&gt;That asymmetry took me longer to internalize than it should have.&lt;/p&gt;
&lt;h2 id="what-has-helped-in-practice"&gt;What has helped in practice
&lt;/h2&gt;&lt;p&gt;A few small habits have helped more than any framework or guideline.&lt;/p&gt;
&lt;p&gt;They line up with one of the points Addy Osmani makes in his chapter on this anti-pattern: by the time the imagined future requirement arrives, you will usually know more about the real problem than you do today. That has held up almost every time I have honored it, and bitten me almost every time I have ignored it.&lt;/p&gt;
&lt;p&gt;Before I add a parameter, abstraction, or configuration layer to a prototype, I try to finish one sentence honestly:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;This exists in this prototype because the specific question being asked requires it.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If I cannot finish the sentence, the abstraction does not ship.&lt;/p&gt;
&lt;p&gt;I also try to write imagined future use cases down in a note or design doc, not in code. A note is cheap. An unused configuration option creates a small maintenance tax that can last for years.&lt;/p&gt;
&lt;p&gt;In design reviews, I try to name the temptation out loud: &lt;em&gt;&amp;ldquo;I wanted to make this generic. I am not going to. Here is why.&amp;rdquo;&lt;/em&gt; Saying it explicitly often makes other people more comfortable doing the same.&lt;/p&gt;
&lt;p&gt;The narrowness of the prototype is not a flaw. It is what lets the prototype answer the question quickly.&lt;/p&gt;
&lt;p&gt;If it is wrong, we can throw it away cheaply.&lt;/p&gt;
&lt;p&gt;If it is right, we can add the abstractions later, with more information than we had at the start.&lt;/p&gt;
&lt;p&gt;YAGNI does not mean ignoring future reuse.&lt;/p&gt;
&lt;p&gt;It means refusing to encode future reuse before the future has produced evidence.&lt;/p&gt;
&lt;h2 id="the-question-i-try-to-use-now"&gt;The question I try to use now
&lt;/h2&gt;&lt;p&gt;When I notice the temptation to generalize too early, I ask:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If I delete this abstraction right now, does the prototype still answer the question it was built to answer?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If the answer is yes, the abstraction does not belong in this version.&lt;/p&gt;
&lt;p&gt;It might belong later, when there is a second concrete case.&lt;/p&gt;
&lt;p&gt;But the next version is not what I am building today.&lt;/p&gt;</description></item></channel></rss>