<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jerrett Davis</title>
    <description>The latest articles on DEV Community by Jerrett Davis (@jerrettdavis).</description>
    <link>https://dev.to/jerrettdavis</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1277980%2F199adce9-2e22-4af1-b425-49361c94cb56.png</url>
      <title>DEV Community: Jerrett Davis</title>
      <link>https://dev.to/jerrettdavis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jerrettdavis"/>
    <language>en</language>
    <item>
      <title>Vibe Engineering: Solving Small Cross-Cutting Concerns</title>
      <dc:creator>Jerrett Davis</dc:creator>
      <pubDate>Tue, 14 Jul 2026 05:16:33 +0000</pubDate>
      <link>https://dev.to/jerrettdavis/vibe-engineering-solving-small-cross-cutting-concerns-35e6</link>
      <guid>https://dev.to/jerrettdavis/vibe-engineering-solving-small-cross-cutting-concerns-35e6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article was originally published on &lt;a href="https://jerrettdavis.com/blog/posts/vibe-engineering-small-cross-cutting-concerns" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Vibe Engineering: Solving Small Cross-Cutting Concerns
&lt;/h1&gt;

&lt;p&gt;I've been enamored by AI technologies far before the current generation of generative large language and diffusion image models hit widespread popularity in the early 2020s. Early in my career, I worked as a developer for a local Oil &amp;amp; Gas focused software company. One of the wings that they ultimately spun off into its own organization revolved around performing predictive data analytics on the massive datasets we'd accumulated across our various customers. Eye-opening stuff. Through properly trained models, the company was able to predict all sorts of failure modes, workflow trigger-points, and actionable business insights. The types of decisions that could be made off of these seemingly meaningless data streams changed how I thought about what software could do.&lt;/p&gt;

&lt;p&gt;After I left that company, I joined a local healthcare organization. We built incredibly insightful visualizations, reports, and dashboards with our wealth of data. We only dipped our toes into training models for actionable predictions, but even those small exercises proved worthwhile. Outbreak forecasting built atop our wide-reaching care network let us glean and plan for valuable resource allocations months in advance.&lt;/p&gt;

&lt;p&gt;A few years later, I found myself working back in oil and gas, this time at a company in a different sector of the field. They too were leveraging AI, training models on pipeline defects, failures, and inspections. This was their bread and butter. Every advancement yielded dividends.&lt;/p&gt;

&lt;p&gt;As the clock ticked ever forward, I landed at yet another organization right as the first real generations of AI-assisted coding tools hit our IDEs. Software focused, this company planted its stake alongside giants like OpenAI's ChatGPT with our own growing suite of AI-assisted offerings. Once again, the capability of these technologies captivated me.&lt;/p&gt;

&lt;p&gt;Around this same time, I started really tinkering with local Large Language Models and diffusion-based image generation. Ollama. Stable Diffusion. Anything I could get my hands on. I was excited enough about the possibilities that I built myself a new PC: a GeForce RTX 4090, 128gb RAM, an Intel i9 14900k (ouch), and a whole bag of other goodies.&lt;/p&gt;

&lt;p&gt;I spared no time getting the machine setup with tooling, utilities, frameworks, and libraries. As the various coding models grew more capable, I found myself leaning into them more, allocating larger and more nebulous tasks their way. For the most part, I used them like a friendlier, more interactive StackOverflow compatriot. I still performed the vast majority of the actual work.&lt;/p&gt;

&lt;p&gt;Then came the last quarter of 2025. I received an offer from another local company. I wasn't exactly looking, but I'd heard great things about the organization. They serenaded me with songs detailing near-endless problems in need of targeted, efficient solutions. Never one to shy away from an interesting challenge, I took the plunge. I quickly found myself back in the office, up to the eyeballs in new codebases and problems I'd never quite encountered before. Sure, the ingredients were familiar. I'd seen variations of these problems throughout my career. But rather than cooking exquisite enterprise dishes, I found myself staring at gas station spaghetti.&lt;/p&gt;

&lt;p&gt;To my coworkers' surprise, I love spaghetti. Paradoxically, perhaps. It was one of my favorite dishes growing up. Unless there was lasagna on the menu, you could rest assured I was getting spaghetti. I loved it so much that I learned to cook it from scratch before I could competently solo-read a Harry Potter novel. Unsurprisingly, spaghetti code was also my first approach to programming, many moons ago. And here I was, hailing from the hometown of the late Tulsa Spaghetti Warehouse, staring back at an all-you-can-eat buffet.&lt;/p&gt;

&lt;p&gt;So I got to chomping.&lt;/p&gt;

&lt;p&gt;Bite after bite, I picked up forks-full of noodles and slurped them up. Meticulously. I started putting names to the dishes. Angel-hair. Spaghettini. Spaghettoni. At the risk of beating an already horrifically abused metaphor, I continued tearing apart one dish at a time. Every second of downtime between onboarding tasks became an opportunity to absorb as much as possible. Not just from the business perspective, but more importantly, from the architectural view.&lt;/p&gt;

&lt;p&gt;The software was home-grown, home-cooked, home-eaten. We had to internally digest it all. The team had mostly come up through the company, many having been there for 15, 20, 30+ years. The code had evolved alongside the company and the individuals who shaped it. Invaluable knowledge lived within those repositories. But it was unstructured. Unrefined.&lt;/p&gt;

&lt;p&gt;There were (and still are) hundreds of codebases comprising the enterprise stack. They're managed by separate and overlapping teams, all focused on propelling the business forward. As the company grew, so did the code. As requirements changed, so did the implementations.&lt;/p&gt;

&lt;p&gt;Littered throughout various repos, you'll find comments. Some truthful. Many lying. All purporting to guide you through a forest of overlapping, stateful, static, threaded operations that seem to have sprouted at random. You clone. You touch. You grep. You cry. You prototype. You bargain. You claw your way through discombobulated systems until, slowly, light begins to shine through the ever-thinning limbs.&lt;/p&gt;

&lt;p&gt;And you start to make sense of what you're looking at.&lt;/p&gt;

&lt;p&gt;Not a forest. No. Something similar, but different. Something closer to a series of massive, interconnected, cautiously cohabitating fungal colonies. Myriad mycological monstrosities expanding into every suitable niche upon which their spores land. As you delicately disentangle their interwoven mycelium, the operation turns into something like a vivisection. Laid bare on the table, the endless nerve-like tendrils begin to make some semblance of sense.&lt;/p&gt;

&lt;p&gt;Slowly but surely, different domain concerns start growing defined boundaries. Lists upon lists are made of identified cross-cutting concerns, domains, state mechanisms, authoritative sources of truth. Chaos soon begins to look a lot like patterns...&lt;/p&gt;




&lt;h2&gt;
  
  
  Little Architectural Building Blocks
&lt;/h2&gt;

&lt;p&gt;Architectural building blocks and design patterns are nothing new. Hundreds, if not thousands, have been coined over the years. One of the "bibles" on the subject, &lt;em&gt;Design Patterns&lt;/em&gt; by Erich Gamma, Richard Helm, Ralph Johnson, and John Vissides, came out while I was still largely quadrupedal. I was probably just taking my first few steps.&lt;/p&gt;

&lt;p&gt;Despite the antiquity of such wisdom, employing these practices isn't the default operating mode for most developers. This continues to surprise me. Most people don't work in the field of their passion. They don't go home and continue thinking and dreaming about their topic of study. Instead, they chase the highest paying fields or follow whatever aptitudes they happen to possess. Silly things like that.&lt;/p&gt;

&lt;p&gt;For better or worse, I was never instilled with such predeterminations.&lt;/p&gt;

&lt;p&gt;Ever since discovering programming in my pre-tweens, I've been intent on wielding that hammer to sink every nail I can find. Over that multi-decade endeavor, I've hit a lot of nails. Also screws. Staples. Thumbs. Drywall. And other imperfect targets. Through that repeated practice (and self-injury), I've managed to beat in a few hard-earned lessons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Good code" is an ever-moving target, but good architecture is eternal.&lt;/li&gt;
&lt;li&gt;You cannot truthfully and confidently deliver a unit of work without pre-defined and validated inputs and outputs.&lt;/li&gt;
&lt;li&gt;Design a system to solve a problem, not to do a task.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  "Good code" is an ever-moving target, but good architecture is eternal
&lt;/h3&gt;

&lt;p&gt;Anyone who's been writing code for longer than an industry-standard release cycle knows things change fast. One week everyone's shoveling the latest ECMAScript hotness down your throat. The next, you're in an intense battle between Rust, Zig, and a bottle of dark liquor. Frameworks, languages, and libraries are a bit like running down to your local big-box hardware store trying to settle on power tools. Certain tools are fantastic for specific problems. You'll find more or less capable versions of similar tools across different brands. The choice matters, but not as much as people think.&lt;/p&gt;

&lt;p&gt;It's often stated that premature optimization is the root of all evil. Agree or disagree with that sentiment, I believe most can agree on one thing: a problem solved via an imperfect solution beats the problem existing unabated.&lt;/p&gt;

&lt;p&gt;What hasn't changed in the 60+ year span of our field? The components of good architecture. From the earliest beginnings, our tooling, languages, and approaches grew around the problems code solved and the strategies used to solve them. Put whatever label you want on the organization: Clean, Hexagonal, Vertical Slice, Domain Driven. The core tenets of creating declarative, functional, problem-domain-centric solutions have held steady through a blustering whirlwind of technological innovation.&lt;/p&gt;

&lt;p&gt;I want to linger on "declarative" for a moment. It's become central to how I think about code quality.&lt;/p&gt;

&lt;p&gt;Declarative code describes &lt;em&gt;what&lt;/em&gt; should happen, not &lt;em&gt;how&lt;/em&gt; it happens. It reads like a specification rather than a set of instructions. It's flat rather than nested. Composable rather than procedural.&lt;/p&gt;

&lt;p&gt;Consider the difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Imperative: a sequence of instructions&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;permissions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoleId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permissions&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GenerateReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;LogSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;LogFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Report generation failed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Declarative: a description of what should happen&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GenerateReport&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;some&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LogSuccess&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;none&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;LogFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Report generation failed"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The declarative version is flatter. No nesting. Each step is a transformation or filter. The flow reads top-to-bottom without branching into ever-deeper indentation levels. You can understand what it does by reading the method names. No need to trace through conditional logic.&lt;/p&gt;

&lt;p&gt;This isn't just aesthetic preference. Flat, declarative code is easier to test because each step is a pure function. Easier to modify because you can add a step without restructuring everything around it. And crucially for our purposes, it's easier for agents to produce correctly. An agent generating the imperative version has to track state, manage nesting levels, handle the combinatorial explosion of possible paths. An agent generating the declarative version? It just chains transformations.&lt;/p&gt;

&lt;p&gt;Package it however you like. As long as your solutions adhere to fundamental software engineering principles, your code can transcend whatever mortal coil its ecosystem imposes upon it.&lt;/p&gt;

&lt;h3&gt;
  
  
  You cannot truthfully and confidently deliver a unit of work without pre-defined and validated inputs and outputs
&lt;/h3&gt;

&lt;p&gt;How can one begin to design a system if they don't know what they're building?&lt;/p&gt;

&lt;p&gt;Too often I see developers blindly feeling their way through a codebase. They coordinate internal and external systems, real and simulated hardware, debuggers, and various other tooling just to pinpoint where they need to work. Then, once they've settled on their platform, they start blindly smashing atoms together hoping to yield something usable.&lt;/p&gt;

&lt;p&gt;Experimentation is great. It's one of the foundations of our field. But experimentation is not for production work.&lt;/p&gt;

&lt;p&gt;That's not to say you can't conduct experiments in production. I have an entire framework dedicated to that cross-cutting concern. But writing and performing experiments around the periphery of your implementations? That's the duty of your testing framework. If you find yourself probing instead of going directly to the test encapsulating your concern, you're working in a malignant, terrifying codebase. Rather than having a system shaped around external business concerns (business-provided inputs, service-provided outputs), you have a black box that requires action to validate. Like trying to guess the components of a modern art shadow projection by staring solely at its stain upon the wall.&lt;/p&gt;

&lt;p&gt;When a developer needs to write code within an application, be it a new feature, bug fix, or otherwise, it's their duty to ensure they have clearly laid out expectations. Paramount among these: expected inputs (configuration, user inputs, workflow steps) and expected outputs (actions performed, values returned, errors that can be thrown). If experimentation is necessary to determine these parameters, that experimentation should be specific, documented, time-bound, with concrete deliverables. Those deliverables should then be cemented through business-relevant behavior-driven tests that enshrine the knowledge gained and mirror the documented requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design a system to solve a problem, not to do a task
&lt;/h3&gt;

&lt;p&gt;I've learned a lot of skills over the years. Varying usefulness. If you need your floor tiled or your cat needs a crocheted vest, I may know a guy who can give some pointers.&lt;/p&gt;

&lt;p&gt;But despite my obsessive compulsion to absorb information like a precious commodity, I lack any desire to repeatedly do things. Don't get me wrong. I can. I've been writing code for decades. But repetitive, monotonous tasks bore me to tears. So I've always tried to learn how to do something, improve it in some way, quantify it, and then automate it as much as humanly possible.&lt;/p&gt;

&lt;p&gt;I'm prone to overusing metaphors. No exception here: I never want to do the digital equivalent of drywall.&lt;/p&gt;

&lt;p&gt;Placing, finishing, and painting drywall are all technically simple endeavors on their surface. Give a sufficiently motivated greenhorn a couple weekends and they'll put up work that competes visually with career tradesmen. That says nothing for their speed or accuracy, or their need to redo tasks as they learn. But the finished product can ultimately be indistinguishable to an end customer.&lt;/p&gt;

&lt;p&gt;Thing is, like many general contractors, I never set out to master one skill. I went out to solve problems and move onto the next. As I found more problems, my skills refined. My approaches improved. In isolation, that's a sustainable model. But over time, across many organizations and codebases, you'll invariably find yourself solving the same problems at a different place with a different toolset. You'll be putting up drywall, mud, and paint rather than selling and refining your new array of drywalling, mudding, and painting robots.&lt;/p&gt;

&lt;p&gt;When solving problems, the work is rarely done in a vacuum. Developers start to get an itch that they've seen something before. I implore you: don't ignore it. Don't just copy and paste the solution that worked last time into the new class with a couple tweaks. Step back. Solve the bigger problem.&lt;/p&gt;

&lt;p&gt;And this is where the modern era offers us something genuinely new.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enter Agents
&lt;/h2&gt;

&lt;p&gt;AI agents have amazing potential. But like overzealous, over-studious summer interns, they lack refinement, direction, and context.&lt;/p&gt;

&lt;p&gt;That load-bearing poster keeping this place standing for the last 20 years? To them, it's just ugly, dated decor of a bygone era, ripe for paving over to make their mark. Within their sponge-like monolith of training data, they've accumulated vast wealth of knowledge. But it's locked away behind ear worms, secret incantations, and raw willpower. These agents are dang near capable of solving any singular task. Unlike the fleshy meatbags that wrote them, though, they lack the criticality, forethought, and analysis paralysis that keeps us humble. That ongoing game of chicken with imposter syndrome? They don't play it.&lt;/p&gt;

&lt;p&gt;Once a problem becomes sufficiently large, it usually becomes multiple problems. Like a cancer metastasizing through the body, problems grow. They multiply. They cripple otherwise healthy solutions. Understanding how to leverage agents to break your problem down and distill it into its most basic essence? That's the key.&lt;/p&gt;

&lt;p&gt;You cannot let them behave like chemo, attempting to find and fix every issue at once. You have to create a single, solvable, provable domain. Then you set them to work there.&lt;/p&gt;

&lt;p&gt;The scope and scale of that domain will vary. Enterprise organizations have their own platforms. Their own logging wrappers, observability, configuration, options, feature flagging, messaging, and so much more. The second something leaks into multiple applications, it's time to figure out how to carve it out. Once a standard has been set, it's much easier to keep it contained, provable, and validated.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Intern Analogy (Extended)
&lt;/h3&gt;

&lt;p&gt;I keep coming back to the intern metaphor because it's genuinely the most apt comparison I've found.&lt;/p&gt;

&lt;p&gt;A fresh intern shows up on day one with a head full of textbook knowledge. Four years of academic projects. And absolutely zero understanding of why your production database has a table called &lt;code&gt;tbl_Users_OLD_DONT_USE_2019&lt;/code&gt;. They're capable. They're eager. They will absolutely demolish your carefully maintained naming conventions if you don't give them explicit guidance.&lt;/p&gt;

&lt;p&gt;Agents operate the same way.&lt;/p&gt;

&lt;p&gt;They've "read" more code than any human could in a lifetime. They know seventeen different ways to implement a repository pattern. They can recite the Gang of Four like scripture. But they don't know that your company uses &lt;code&gt;ICommandHandler&amp;lt;T&amp;gt;&lt;/code&gt; instead of &lt;code&gt;IHandler&amp;lt;T&amp;gt;&lt;/code&gt; because someone made that decision in 2017 and it stuck. They don't know that the &lt;code&gt;Utils&lt;/code&gt; folder is actually load-bearing legacy powering three critical batch jobs. They don't know that Jerry from accounting will lose his mind if you rename the &lt;code&gt;GetReportData&lt;/code&gt; stored procedure because he's been calling it directly from Excel for seven years.&lt;/p&gt;

&lt;p&gt;Context is everything. And context is precisely what agents lack until you give it to them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tools, Not Decisions
&lt;/h3&gt;

&lt;p&gt;There's a fundamental distinction in how you can leverage agents that took me longer to articulate than I'd like to admit.&lt;/p&gt;

&lt;p&gt;You want agents to build tools that you validate. Not to make decisions that you trust.&lt;/p&gt;

&lt;p&gt;The difference is subtle but profound. When an agent makes a decision directly (choosing an architecture, selecting a library, determining a business rule), you're trusting its judgment. As we've established, agents have plenty of knowledge but questionable judgment. They don't know your context. They can't weigh your constraints. They'll confidently recommend the "best practice" solution that's completely wrong for your situation.&lt;/p&gt;

&lt;p&gt;But when an agent builds a tool? The dynamic shifts entirely.&lt;/p&gt;

&lt;p&gt;The tool has inputs and outputs. It can be tested. Validated. Run against known cases and verified to produce correct results. You're not trusting the agent's judgment anymore. You're trusting your own ability to verify the tool's behavior.&lt;/p&gt;

&lt;p&gt;This is why I've become increasingly obsessed with having agents build source generators, analyzers, and code scaffolding rather than writing application code directly. A source generator is a tool. It takes input (your domain model, your configuration, your conventions) and produces output (the boilerplate code you'd otherwise write by hand). You can inspect that output. Test it. Verify the generator produces correct code for all your known cases.&lt;/p&gt;

&lt;p&gt;Compare this to having an agent write your repository implementations directly. Now you're reviewing each repository individually. Trusting that the agent understood your conventions for this specific case. Hoping it didn't introduce subtle bugs that won't surface until production.&lt;/p&gt;

&lt;p&gt;With a generator, you validate the generator once. Then it produces correct code forever, across all your entities, without additional review. The agent helped you build a tool, and the tool does the work. You validated the tool's logic, not the agent's judgment.&lt;/p&gt;

&lt;p&gt;This principle extends beyond source generators. Configuration validators. Migration scripts. Test scaffolds. Documentation generators. Any time you can have an agent build a tool that you validate rather than making decisions you trust, you're in a better position.&lt;/p&gt;

&lt;p&gt;The mantra I've adopted: "Agents build tools. Tools do work. I validate tools."&lt;/p&gt;

&lt;h3&gt;
  
  
  Scoping the Work
&lt;/h3&gt;

&lt;p&gt;The trick, then, is to scope your agent's work to a domain small enough that context can be fully provided, yet large enough that the work is meaningful. This is not unlike the advice given to junior developers: don't let them architect the whole system, but don't insult them with busywork either. Find the Goldilocks zone where they can produce real value with appropriate guardrails.&lt;/p&gt;

&lt;p&gt;For me, that zone tends to be cross-cutting concerns. Not the core business logic (that requires too much institutional knowledge), and not simple CRUD operations (those are usually faster to do yourself than to explain). The sweet spot lies in the infrastructure layer: the logging wrappers, the configuration abstractions, the messaging contracts, the feature flag interfaces. These are bounded problems with clear inputs and outputs, well-established patterns in the broader ecosystem, and minimal business-specific context required.&lt;/p&gt;

&lt;p&gt;And crucially, these concerns lend themselves to the "tools, not decisions" approach. A logging wrapper is a tool. A configuration binder is a tool. A message serializer is a tool. You can define what these tools should do, have an agent implement them, validate they work correctly, and then trust them going forward. You're not asking the agent to make business decisions. You're asking it to build infrastructure that you can verify.&lt;/p&gt;

&lt;p&gt;Consider a logging wrapper. Every enterprise application needs one. The requirements are almost universal: structured logging, correlation IDs, log levels, maybe some redaction for sensitive data. The problem is well-understood, the patterns are documented, and an agent can produce a solid implementation if you give it your specific constraints (target framework, existing dependencies, naming conventions, test requirements). More importantly, you can write tests that verify the wrapper behaves correctly: does it include correlation IDs? Does it redact sensitive fields? Does it respect log level configuration? These are all verifiable properties of a tool.&lt;/p&gt;

&lt;p&gt;Compare that to implementing a new pricing algorithm. The agent would need to understand your product catalog structure, your customer segmentation model, your promotional rules, your tax jurisdictions, your integration points with the billing system, and probably a dozen other things that live in the heads of people who've been there for a decade. That's not a bounded problem. That's a swamp. And worse, it's a decision problem, not a tool problem. The "correctness" of a pricing algorithm depends on business judgment, not technical verification.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Vibe Engineering Workflow
&lt;/h3&gt;

&lt;p&gt;I've started calling my approach "vibe engineering" (somewhat tongue-in-cheek) because it's less about prescriptive methodology and more about cultivating the right relationship between human architect and AI assistant. The "vibe" is the combination of context, constraints, and creative latitude you establish before any code gets written.&lt;/p&gt;

&lt;p&gt;The workflow looks something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identify&lt;/strong&gt; a cross-cutting concern that appears in multiple places&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report&lt;/strong&gt; on its current state (how it's implemented, where it varies, what problems it causes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan&lt;/strong&gt; the extraction (define boundaries, interfaces, contracts)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build&lt;/strong&gt; the solution (iteratively, with human review at each stage)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintain&lt;/strong&gt; the solution (documentation, versioning, evolution)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these stages has its own dynamics when working with agents. The first two (identify and report) are where agents truly shine, because they can process vast amounts of code faster than any human. The middle stage (plan) requires heavy human involvement, because this is where architectural decisions get made. The build stage is collaborative, with the agent doing the heavy lifting under human supervision. And the maintenance stage is where the investment pays dividends, because a well-documented, well-tested solution can be maintained with minimal ongoing effort.&lt;/p&gt;

&lt;p&gt;Let's dig into each of these stages.&lt;/p&gt;




&lt;h2&gt;
  
  
  Identifying Cross-Cutting Concerns
&lt;/h2&gt;

&lt;p&gt;The first step in any extraction effort is knowing what to extract. This sounds obvious, but in practice, cross-cutting concerns have a way of hiding in plain sight. They're the code patterns you've copied so many times they feel like native syntax. They're the utility classes that exist in seventeen different namespaces because everyone needed one and nobody thought to check if it already existed. They're the configuration patterns that vary just enough between applications to be annoying but not enough to be obviously wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Teaching an Agent to See Patterns
&lt;/h3&gt;

&lt;p&gt;Agents are exceptionally good at this phase of the work. Give them access to a codebase (or several), provide some guidance on what you're looking for, and they can surface patterns faster than any human could through manual grep sessions.&lt;/p&gt;

&lt;p&gt;But here's a nuance that ties back to our "tools, not decisions" principle: for one-off analysis, having an agent describe what it finds is fine. For ongoing pattern detection, you want the agent to build you an analyzer that you can run repeatedly. The difference matters.&lt;/p&gt;

&lt;p&gt;If you ask an agent "what HTTP client patterns exist in this codebase?" you'll get an answer based on its current understanding of the code at this moment. If you ask an agent "build me a Roslyn analyzer that detects non-standard HTTP client instantiation patterns," you get a tool you can run in CI, that will catch new violations as they're introduced, and whose detection logic you can inspect and verify.&lt;/p&gt;

&lt;p&gt;For the initial discovery phase, conversational analysis is perfectly appropriate. But once you've identified a concern worth tracking, consider whether the detection logic should become a tool.&lt;/p&gt;

&lt;p&gt;The key is being specific about what constitutes a "cross-cutting concern" in your context. For a new codebase, I typically start with a prompt structure something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Analyze the following repositories for cross-cutting concerns.
A cross-cutting concern is any functionality that:
1. Appears in multiple applications or services
2. Is not core business logic
3. Could reasonably be extracted into a shared library
4. Currently has inconsistent implementations across the codebase

Focus specifically on:
- Logging and observability patterns
- Configuration and options binding
- Error handling and exception management
- HTTP client usage and resilience patterns
- Caching strategies
- Validation approaches
- Mapping and transformation utilities

For each concern you identify, provide:
- Where it appears (specific files/projects)
- How implementations vary
- What problems the inconsistency causes
- A preliminary assessment of extraction complexity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output from this kind of analysis is often illuminating. In one recent engagement, an agent identified that the organization had no fewer than twelve different approaches to HTTP client instantiation across their service fleet. Some used raw &lt;code&gt;HttpClient&lt;/code&gt; (with all the socket exhaustion problems that entails). Some used &lt;code&gt;IHttpClientFactory&lt;/code&gt; correctly. Some used it incorrectly. A few had hand-rolled retry logic. Others used Polly but configured it differently. One particularly creative implementation had a static &lt;code&gt;HttpClient&lt;/code&gt; wrapped in a &lt;code&gt;lock&lt;/code&gt; statement "for thread safety."&lt;/p&gt;

&lt;p&gt;None of this was visible from any single repository. It only became apparent when you looked across the portfolio. And that's exactly the kind of analysis that agents excel at.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Enumeration Phase
&lt;/h3&gt;

&lt;p&gt;Once you've identified candidate concerns, the next step is enumeration: building a comprehensive inventory of where each concern appears and how it's currently implemented. This is tedious work for humans but trivial for agents.&lt;/p&gt;

&lt;p&gt;For the HTTP client example, the enumeration might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP Client Implementations Inventory:

1. ServiceA/Infrastructure/HttpClientWrapper.cs
   - Pattern: Static HttpClient with manual DNS refresh
   - Retry logic: None
   - Timeout: 30 seconds (hardcoded)
   - Dependencies: None

2. ServiceB/Clients/ExternalApiClient.cs
   - Pattern: IHttpClientFactory via constructor injection
   - Retry logic: Polly with 3 retries, exponential backoff
   - Timeout: Configured via appsettings.json
   - Dependencies: Polly, Microsoft.Extensions.Http

3. ServiceC/Utilities/ApiHelper.cs
   - Pattern: New HttpClient per request (!)
   - Retry logic: Manual try-catch with Thread.Sleep
   - Timeout: 100 seconds (default)
   - Dependencies: None

[... and so on for each occurrence ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This inventory becomes the foundation for the next phase. You can't plan an extraction without knowing what you're extracting from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Categorizing by Extraction Complexity
&lt;/h3&gt;

&lt;p&gt;Not all cross-cutting concerns are equally amenable to extraction. Some are straightforward (everyone's doing roughly the same thing, just in different places). Others are deeply entangled with business logic or have subtle behavioral differences that must be preserved.&lt;/p&gt;

&lt;p&gt;I typically categorize concerns into three buckets:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low complexity&lt;/strong&gt;: The implementations are nearly identical, differ only in configuration, and have no dependencies on application-specific types. These can often be extracted in a day or two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medium complexity&lt;/strong&gt;: The implementations share a common core but have legitimate variations that need to be accommodated through configuration, extension points, or strategy patterns. These might take a week or two to extract properly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High complexity&lt;/strong&gt;: The implementations have diverged significantly, are entangled with business logic, or have undocumented behavioral quirks that applications depend on. These require careful analysis and potentially phased migration strategies.&lt;/p&gt;

&lt;p&gt;Agents can help with this categorization, but human judgment is essential. An agent might see two implementations that look different and assume high complexity, when in reality one is just a worse version of the other that can be safely replaced. Or it might see two implementations that look similar but miss subtle semantic differences that matter enormously in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the Concern Registry
&lt;/h3&gt;

&lt;p&gt;The output of this phase should be a structured registry of identified concerns, something that can be referenced throughout the extraction effort and maintained going forward. I like to keep these as markdown files in a dedicated repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Cross-Cutting Concerns Registry&lt;/span&gt;

&lt;span class="gu"&gt;## HTTP Client Management&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Status**&lt;/span&gt;: Identified, awaiting prioritization
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Complexity**&lt;/span&gt;: Medium
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Occurrences**&lt;/span&gt;: 12 services
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Primary variations**&lt;/span&gt;: Retry policies, timeout configurations, authentication handling
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Proposed solution**&lt;/span&gt;: Shared library with configurable policies
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Estimated effort**&lt;/span&gt;: 2 weeks extraction, 4 weeks migration

&lt;span class="gu"&gt;## Structured Logging&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Status**&lt;/span&gt;: In progress
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Complexity**&lt;/span&gt;: Low
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Occurrences**&lt;/span&gt;: 23 services
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Primary variations**&lt;/span&gt;: Log format, correlation ID propagation
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Proposed solution**&lt;/span&gt;: Thin wrapper over Serilog with standard enrichers
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Estimated effort**&lt;/span&gt;: 1 week extraction, 2 weeks migration

[... additional concerns ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This registry serves multiple purposes. It documents what you've found. It helps prioritize extraction efforts. It provides a reference for developers who might otherwise duplicate these concerns yet again. And it becomes the basis for the architectural decision records you'll create in the planning phase.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reporting and Analysis
&lt;/h2&gt;

&lt;p&gt;Once you've identified and enumerated your cross-cutting concerns, the next challenge is communicating what you've found. This is where many technical initiatives die: not because the analysis was wrong, but because it never made it out of the developer's head and into a form that others could act on.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture Decision Record
&lt;/h3&gt;

&lt;p&gt;For each significant cross-cutting concern you plan to address, I strongly recommend creating an Architecture Decision Record (ADR). These documents serve as both analysis and proposal, capturing the current state, the problem it causes, the proposed solution, and the trade-offs involved.&lt;/p&gt;

&lt;p&gt;Agents are surprisingly good at drafting ADRs, particularly the background and analysis sections. They can synthesize the enumeration data you gathered earlier into a coherent narrative. The key is to review and refine their output, adding the organizational context and political considerations that agents can't know.&lt;/p&gt;

&lt;p&gt;A typical ADR structure might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# ADR-007: Standardize HTTP Client Management&lt;/span&gt;

&lt;span class="gu"&gt;## Status&lt;/span&gt;
Proposed

&lt;span class="gu"&gt;## Context&lt;/span&gt;
Our service portfolio currently contains 12 different approaches to HTTP
client management. This inconsistency has led to:
&lt;span class="p"&gt;
-&lt;/span&gt; Production incidents due to socket exhaustion (ServiceC, ServiceF)
&lt;span class="p"&gt;-&lt;/span&gt; Inconsistent retry behavior causing cascading failures
&lt;span class="p"&gt;-&lt;/span&gt; Security vulnerabilities from improper certificate validation
&lt;span class="p"&gt;-&lt;/span&gt; Developer confusion and duplicated effort

Analysis of the current implementations reveals three main categories:
[detailed breakdown follows]

&lt;span class="gu"&gt;## Decision&lt;/span&gt;
We will create a shared library (&lt;span class="sb"&gt;`Company.Http.Resilience`&lt;/span&gt;) that provides:
&lt;span class="p"&gt;
1.&lt;/span&gt; Pre-configured HttpClient instances via IHttpClientFactory
&lt;span class="p"&gt;2.&lt;/span&gt; Standard retry policies with configurable parameters
&lt;span class="p"&gt;3.&lt;/span&gt; Circuit breaker integration
&lt;span class="p"&gt;4.&lt;/span&gt; Consistent timeout handling
&lt;span class="p"&gt;5.&lt;/span&gt; Structured logging of HTTP operations
&lt;span class="p"&gt;6.&lt;/span&gt; Health check integration

&lt;span class="gu"&gt;## Consequences&lt;/span&gt;

&lt;span class="gu"&gt;### Positive&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Consistent behavior across all services
&lt;span class="p"&gt;-&lt;/span&gt; Reduced incident rate from HTTP-related issues
&lt;span class="p"&gt;-&lt;/span&gt; Faster onboarding for new developers
&lt;span class="p"&gt;-&lt;/span&gt; Single place to update security configurations

&lt;span class="gu"&gt;### Negative&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Migration effort required for existing services
&lt;span class="p"&gt;-&lt;/span&gt; Learning curve for teams unfamiliar with new patterns
&lt;span class="p"&gt;-&lt;/span&gt; Potential for breaking changes if not carefully managed

&lt;span class="gu"&gt;### Neutral&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Requires ongoing maintenance of shared library
&lt;span class="p"&gt;-&lt;/span&gt; Need to establish governance for library updates

&lt;span class="gu"&gt;## Migration Strategy&lt;/span&gt;
[phased approach details]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Generating Reports for Different Audiences
&lt;/h3&gt;

&lt;p&gt;One of the underappreciated aspects of this work is that different stakeholders need different views of the same information. A detailed technical analysis is great for the architecture team, but it's not what you show in a sprint planning meeting or a budget request.&lt;/p&gt;

&lt;p&gt;Agents can help generate multiple views from the same underlying data:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Executive summary&lt;/strong&gt; (for leadership):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We've identified 12 inconsistent HTTP client implementations across our
services. This inconsistency contributed to 3 production incidents last
quarter and consumes approximately 40 developer-hours per month in
debugging and maintenance. A 2-week investment to create a standardized
library would eliminate this overhead and reduce incident risk.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Technical summary&lt;/strong&gt; (for architecture review):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cross-cutting concern: HTTP client management
Current state: 12 implementations, 4 distinct patterns
Risk level: High (socket exhaustion, retry storms)
Proposed solution: Shared library with IHttpClientFactory, Polly policies
Effort estimate: 80 hours extraction, 160 hours migration
Dependencies: Microsoft.Extensions.Http, Polly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Developer guide&lt;/strong&gt; (for implementation teams):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Migrating to Company.Http.Resilience&lt;/span&gt;

&lt;span class="gu"&gt;## Why are we doing this?&lt;/span&gt;
[brief context]

&lt;span class="gu"&gt;## What changes for me?&lt;/span&gt;
[concrete before/after examples]

&lt;span class="gu"&gt;## How do I migrate?&lt;/span&gt;
[step-by-step instructions]

&lt;span class="gu"&gt;## What if I need custom behavior?&lt;/span&gt;
[extension points and configuration options]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Analysis Artifact Pipeline
&lt;/h3&gt;

&lt;p&gt;I've found it useful to establish a standard pipeline for producing these artifacts. When a new cross-cutting concern is identified, the pipeline kicks in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Raw enumeration&lt;/strong&gt; (agent-generated): List of all occurrences with code snippets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Categorized analysis&lt;/strong&gt; (agent-generated, human-reviewed): Complexity assessment, variation analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ADR draft&lt;/strong&gt; (agent-generated, human-refined): Formal decision record&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Executive summary&lt;/strong&gt; (agent-generated from ADR): One-page overview&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration guide&lt;/strong&gt; (agent-generated after solution is built): Developer-facing documentation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each artifact builds on the previous ones, and the agent can be prompted to maintain consistency across them. If the ADR changes, the executive summary and migration guide should be regenerated to reflect those changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maintaining the Paper Trail
&lt;/h3&gt;

&lt;p&gt;Documentation has a half-life. The moment you finish writing it, it starts decaying. Requirements change, implementations evolve, and that carefully crafted ADR becomes a historical curiosity rather than a living reference.&lt;/p&gt;

&lt;p&gt;This is where the combination of agents and good tooling pays dividends. If your analysis artifacts are stored in version control (and they should be), you can periodically prompt an agent to review them against the current codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review ADR-007 against the current state of our HTTP client implementations.
Identify any drift between the documented decision and actual implementation.
Flag any new services that haven't adopted the standard library.
Suggest updates to the ADR if the decision should be revised.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of maintenance review is exactly the sort of tedious, important work that humans tend to skip and agents can do reliably. Schedule it quarterly, tie it to your release cycles, or trigger it whenever significant changes are merged to the shared libraries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Planning the Solution
&lt;/h2&gt;

&lt;p&gt;This is where human judgment becomes essential. Agents can identify patterns, enumerate occurrences, and draft documentation, but they cannot make architectural decisions. They don't understand your organization's priorities, your team's capabilities, your deployment constraints, or the political landscape that determines what solutions are actually viable.&lt;/p&gt;

&lt;p&gt;Planning is collaborative, but the human must drive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining Boundaries
&lt;/h3&gt;

&lt;p&gt;The first planning decision is scope: what exactly are you extracting, and what are you leaving behind?&lt;/p&gt;

&lt;p&gt;This sounds simple, but it's where most extraction efforts go wrong. The temptation is to solve everything at once. You start with "let's standardize HTTP client management" and end up with "let's create a comprehensive platform SDK that handles HTTP, logging, configuration, caching, authentication, and messaging." Six months later, you've built a framework that nobody wants to adopt because it's too opinionated, too complex, and too different from what teams are currently using.&lt;/p&gt;

&lt;p&gt;The antidote is aggressive scoping. For each cross-cutting concern, define:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's in scope:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The specific functionality being extracted&lt;/li&gt;
&lt;li&gt;The configuration surface area&lt;/li&gt;
&lt;li&gt;The extension points (if any)&lt;/li&gt;
&lt;li&gt;The test coverage requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What's explicitly out of scope:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Related functionality that could be added later&lt;/li&gt;
&lt;li&gt;Integrations with other concerns&lt;/li&gt;
&lt;li&gt;Business-specific customizations&lt;/li&gt;
&lt;li&gt;Anything that would delay the initial release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write these boundaries down. Put them in the ADR. Refer back to them when you (or your agent) start gold-plating.&lt;/p&gt;

&lt;h3&gt;
  
  
  Designing the Interface Contract
&lt;/h3&gt;

&lt;p&gt;Before any implementation begins, you need to define the contract that consuming applications will depend on. This is the most important artifact you'll produce, because it's the hardest to change later.&lt;/p&gt;

&lt;p&gt;For a shared library, the contract includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public types (interfaces, classes, records, enums)&lt;/li&gt;
&lt;li&gt;Configuration schema (options classes, configuration sections)&lt;/li&gt;
&lt;li&gt;Behavioral guarantees (what happens on failure, what gets logged, what metrics are emitted)&lt;/li&gt;
&lt;li&gt;Dependencies (what NuGet packages are required, what framework versions are supported)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I typically start by writing the consuming code first. Before any implementation exists, write how you want developers to use the library. This outside-in approach ensures that the interface is designed for the consumer, not for the implementer. It's the same philosophy that drives behavior-driven development: start with what you want to observe, then work backward to what you need to build.&lt;/p&gt;

&lt;h4&gt;
  
  
  Fluent Over Nested
&lt;/h4&gt;

&lt;p&gt;When designing interfaces, I have a strong bias toward fluent APIs. A fluent API chains method calls, reads left-to-right (or top-to-bottom), and minimizes the cognitive load required to understand what's happening.&lt;/p&gt;

&lt;p&gt;Compare these two approaches to configuring an HTTP client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Nested/Constructor-heavy approach&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ResilientHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClientOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;baseAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;retryPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RetryPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;backoffType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackoffType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exponential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;initialDelay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;circuitBreaker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CircuitBreakerOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;failureThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;samplingDuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;breakDuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fluent approach&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ResilientHttpClient&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.example.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;retry&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MaxAttempts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExponentialBackoff&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitialDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithCircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;breaker&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;breaker&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TripAfter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;FailuresIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StayOpenFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fluent version is longer in line count but dramatically easier to read. Each configuration concern is isolated to its own chain. You can understand the retry policy without mentally parsing nested constructor arguments. The indentation is shallow and consistent.&lt;/p&gt;

&lt;p&gt;This matters for agent-assisted development in two ways. First, when you ask an agent to implement a fluent API, it's forced to think about each configuration concern in isolation. It can't hide complexity in nested constructors. Second, when you ask an agent to &lt;em&gt;use&lt;/em&gt; a fluent API, the resulting code is more likely to be correct because each step is self-documenting.&lt;/p&gt;

&lt;p&gt;I've built entire libraries around this philosophy. &lt;a href="https://github.com/JerrettDavis/PatternKit" rel="noopener noreferrer"&gt;PatternKit&lt;/a&gt; provides fluent implementations of Gang of Four patterns precisely because the traditional examples are unnecessarily verbose and nested. &lt;a href="https://github.com/JerrettDavis/TinyBDD" rel="noopener noreferrer"&gt;TinyBDD&lt;/a&gt; uses a fluent Given/When/Then syntax because test scenarios should read like specifications, not like procedural code.&lt;/p&gt;

&lt;p&gt;When planning your extracted libraries, default to fluent. Your future self (and your agents) will thank you.&lt;/p&gt;

&lt;h4&gt;
  
  
  Design for Validation
&lt;/h4&gt;

&lt;p&gt;But there's another dimension to consider: design for validation. Every interface you create should be testable. Every behavior should be observable. Every configuration should be verifiable. If you can't write a test that proves the component works correctly, your interface is wrong.&lt;/p&gt;

&lt;p&gt;This is especially important when agents will be involved in the implementation. Remember: you want to validate tools, not trust decisions. That means your interfaces need to expose seams where validation can occur. Opaque interfaces that hide all internal state make it impossible to verify correct behavior. Transparent interfaces with clear inputs, outputs, and observable side effects make validation straightforward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This is what we WANT the consuming code to look like&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IResilientHttpClient&lt;/span&gt; &lt;span class="n"&gt;_http&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;MyService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IResilientHttpClient&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_http&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetCustomerAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Simple case: just make the call, resilience is handled for us&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/customers/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;PlaceOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Custom configuration for this specific call&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;HttpCallOptions&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;RetryPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RetryPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;// Orders shouldn't auto-retry&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PostAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the registration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In Startup or Program.cs&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddResilientHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"ApiBaseUrl"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RetryPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RetryPolicyOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;MaxRetries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;BackoffType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BackoffType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exponential&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CircuitBreaker&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CircuitBreakerOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FailureThreshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;SamplingDuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;BreakDuration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This "outside-in" approach ensures that the interface is designed for the consumer, not for the implementer. It's the same philosophy that drives behavior-driven development: start with what you want to observe, then work backward to what you need to build.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leveraging Agents for Interface Exploration
&lt;/h3&gt;

&lt;p&gt;While the human defines the desired interface, agents can be invaluable for exploring the design space. Give them the proposed interface and ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review this proposed interface for a resilient HTTP client library.
Consider:
- Usability: Is this intuitive for developers to use?
- Flexibility: Can it accommodate the variations we found in our analysis?
- Testability: Can consuming code easily mock or stub this?
- Consistency: Does it follow .NET conventions and patterns?
- Evolution: Can we extend this without breaking changes?

Identify potential issues and suggest alternatives.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent might surface considerations you hadn't thought of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"The &lt;code&gt;RetryPolicy.None&lt;/code&gt; option might be confusing; consider &lt;code&gt;RetryPolicy.Disabled&lt;/code&gt; for clarity."&lt;/li&gt;
&lt;li&gt;"You're using a concrete &lt;code&gt;HttpCallOptions&lt;/code&gt; class; an interface would improve testability."&lt;/li&gt;
&lt;li&gt;"The circuit breaker configuration is complex; consider providing named presets like &lt;code&gt;CircuitBreaker.Aggressive&lt;/code&gt; and &lt;code&gt;CircuitBreaker.Conservative&lt;/code&gt;."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not all suggestions will be good, but the exploration is valuable. It's like having a code review before the code exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the Behavior Specification
&lt;/h3&gt;

&lt;p&gt;Once the interface is defined, the next step is specifying its behavior. This is where TinyBDD (or your BDD framework of choice) shines. Write the scenarios that define what the library must do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Resilient HTTP Client"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResilientHttpClientScenarios&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TinyBddXunitBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Successful request returns deserialized response"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SuccessfulRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a configured resilient HTTP client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="nf"&gt;CreateTestClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CustomerData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Test"&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"making a GET request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"/customers/123"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"the response is deserialized correctly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"123"&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertPassed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Transient failure triggers retry"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;TransientFailureRetry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a client configured with 3 retries"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="nf"&gt;CreateTestClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failTimes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thenSucceed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"making a request that fails twice then succeeds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"/customers/123"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"the request ultimately succeeds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"exactly 3 attempts were made"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="nf"&gt;GetAttemptCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertPassed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Circuit breaker opens after threshold"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CircuitBreakerOpens&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a client with circuit breaker (threshold: 3 failures)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="nf"&gt;CreateTestClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alwaysFail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;circuitBreakerThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"making 5 failing requests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"/customers/123"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* expected */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"the circuit breaker is open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCircuitBreakerOpen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"later requests fail fast without attempting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sw&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartNew&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"/customers/123"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* expected */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ElapsedMilliseconds&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Should fail immediately&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertPassed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These scenarios become the acceptance criteria for your implementation. They define "done." They're the contract between you (the architect) and whoever implements the library (possibly an agent, possibly a developer, possibly you at 2 AM after too much coffee).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Planning Artifact
&lt;/h3&gt;

&lt;p&gt;The output of the planning phase should be a comprehensive specification that can be handed to an implementer (human or otherwise):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Interface definitions&lt;/strong&gt; (the public API surface)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavior scenarios&lt;/strong&gt; (the BDD specs that define correctness)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration schema&lt;/strong&gt; (what options are available and what they do)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency list&lt;/strong&gt; (what packages are required)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-functional requirements&lt;/strong&gt; (performance targets, compatibility requirements)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Out-of-scope list&lt;/strong&gt; (what this version explicitly doesn't do)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With these artifacts in hand, the implementation phase becomes almost mechanical. The creative work is done. The decisions are made. Now it's just a matter of making the scenarios pass.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building Domain-Specific Solutions
&lt;/h2&gt;

&lt;p&gt;With a clear specification in hand, implementation becomes the most straightforward phase of the entire process. This is where agents really earn their keep. They're not making architectural decisions (those are already made). They're not interpreting vague requirements (those are already specified as scenarios). They're simply writing code to make tests pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Implementation Loop
&lt;/h3&gt;

&lt;p&gt;The implementation workflow follows a tight loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give the agent the interface definition and one or two scenarios&lt;/li&gt;
&lt;li&gt;Have it produce an implementation&lt;/li&gt;
&lt;li&gt;Run the scenarios&lt;/li&gt;
&lt;li&gt;If they fail, provide the failure output and iterate&lt;/li&gt;
&lt;li&gt;If they pass, move to the next batch of scenarios&lt;/li&gt;
&lt;li&gt;Repeat until all scenarios are green&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not unlike test-driven development, except the agent is doing most of the typing. You're still driving the process, reviewing the output, and making judgment calls when the agent gets stuck or produces something questionable.&lt;/p&gt;

&lt;h4&gt;
  
  
  Guiding Toward Flat, Functional Implementations
&lt;/h4&gt;

&lt;p&gt;When prompting for implementations, I explicitly request flat, functional code. This isn't just stylistic preference; it directly impacts the quality of agent-generated code.&lt;/p&gt;

&lt;p&gt;Agents have a tendency to produce deeply nested, imperative code when left to their defaults. They've been trained on millions of lines of code, and unfortunately, a lot of that code is nested spaghetti. If you don't specify otherwise, you'll get &lt;code&gt;if&lt;/code&gt; statements inside &lt;code&gt;try&lt;/code&gt; blocks inside &lt;code&gt;foreach&lt;/code&gt; loops inside &lt;code&gt;while&lt;/code&gt; conditions.&lt;/p&gt;

&lt;p&gt;Counter this by being explicit in your prompts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Implement&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="n"&gt;logic&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;IResilientHttpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;Style&lt;/span&gt; &lt;span class="n"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Prefer&lt;/span&gt; &lt;span class="n"&gt;pure&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="n"&gt;over&lt;/span&gt; &lt;span class="n"&gt;stateful&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Use&lt;/span&gt; &lt;span class="n"&gt;early&lt;/span&gt; &lt;span class="n"&gt;returns&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;avoid&lt;/span&gt; &lt;span class="n"&gt;nesting&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Chain&lt;/span&gt; &lt;span class="n"&gt;operations&lt;/span&gt; &lt;span class="n"&gt;rather&lt;/span&gt; &lt;span class="n"&gt;than&lt;/span&gt; &lt;span class="n"&gt;nesting&lt;/span&gt; &lt;span class="n"&gt;them&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Each&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="n"&gt;should&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Avoid&lt;/span&gt; &lt;span class="n"&gt;mutable&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;possible&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Use&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="n"&gt;matching&lt;/span&gt; &lt;span class="n"&gt;over&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;chains&lt;/span&gt;

&lt;span class="n"&gt;Example&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;functional&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;codebase&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference in output quality is stark. With explicit style guidance, you get code like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExecuteWithRetryAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RetryPolicy&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attempts&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;TryExecute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstSuccessOrThrowAggregate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TryExecute&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;IsTransient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;DelayForAttempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without style guidance, you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExecuteWithRetryAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RetryPolicy&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;exceptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;IsTransient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxAttempts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;CalculateDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AggregateException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both might work, but the first is easier to test, easier to modify, and easier to reason about. The functional version has clear data flow. Each function transforms input to output. The imperative version has hidden state (&lt;code&gt;attempts&lt;/code&gt;, &lt;code&gt;exceptions&lt;/code&gt;), multiple exit points, and nested control flow.&lt;/p&gt;

&lt;p&gt;A typical prompting session might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Implement&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;IResilientHttpClient&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;according&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;specification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Start&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;basic&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="n"&gt;flow&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;don&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="n"&gt;worry&lt;/span&gt; &lt;span class="n"&gt;about&lt;/span&gt; &lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="n"&gt;logic&lt;/span&gt; &lt;span class="n"&gt;yet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;Interface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;Scenario&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;satisfy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="s"&gt;"Successful request returns deserialized response"&lt;/span&gt; &lt;span class="n"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;Constraints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Use&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;internally&lt;/span&gt; &lt;span class="n"&gt;via&lt;/span&gt; &lt;span class="n"&gt;IHttpClientFactory&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Use&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Json&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;serialization&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Follow&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NET&lt;/span&gt; &lt;span class="n"&gt;naming&lt;/span&gt; &lt;span class="n"&gt;conventions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Include&lt;/span&gt; &lt;span class="n"&gt;XML&lt;/span&gt; &lt;span class="n"&gt;documentation&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent produces an implementation. You run the scenario. It passes (or doesn't). You iterate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling Implementation Complexity
&lt;/h3&gt;

&lt;p&gt;Not all scenarios are equally straightforward. The basic happy path is usually trivial. The edge cases are where implementations get interesting.&lt;/p&gt;

&lt;p&gt;For complex scenarios like circuit breaker behavior, I've found it helpful to break the implementation into phases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: Core flow&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic request/response&lt;/li&gt;
&lt;li&gt;Serialization/deserialization&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: Retry logic&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configurable retry policies&lt;/li&gt;
&lt;li&gt;Exponential backoff&lt;/li&gt;
&lt;li&gt;Jitter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: Circuit breaker&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Failure tracking&lt;/li&gt;
&lt;li&gt;State transitions (closed -&amp;gt; open -&amp;gt; half-open)&lt;/li&gt;
&lt;li&gt;Recovery logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 4: Observability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured logging&lt;/li&gt;
&lt;li&gt;Metrics emission&lt;/li&gt;
&lt;li&gt;Correlation ID propagation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each phase has its own scenarios, and each phase builds on the previous one. The agent doesn't need to solve everything at once. It just needs to make the current phase's scenarios pass without breaking the previous phases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Review and Refinement
&lt;/h3&gt;

&lt;p&gt;Agent-generated code needs review. Always. Without exception.&lt;/p&gt;

&lt;p&gt;The good news is that reviewing code is much faster than writing it. And because you have comprehensive scenarios, you can focus your review on concerns that tests don't catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Style and convention&lt;/strong&gt;: Does this match your existing codebase?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Are there obvious inefficiencies?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: Are there potential vulnerabilities?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainability&lt;/strong&gt;: Will future developers understand this?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge cases&lt;/strong&gt;: Are there scenarios we didn't think to specify?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I typically do a first pass looking for obvious issues, then run the code through any static analysis tools we use (Roslyn analyzers, SonarQube, etc.), then do a deeper read of any code that handles security or concurrency.&lt;/p&gt;

&lt;p&gt;The agent can help with refinement too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Review&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;potential&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;Specifically&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Thread&lt;/span&gt; &lt;span class="n"&gt;safety&lt;/span&gt; &lt;span class="n"&gt;concerns&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Resource&lt;/span&gt; &lt;span class="n"&gt;disposal&lt;/span&gt; &lt;span class="n"&gt;issues&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Potential&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="n"&gt;reference&lt;/span&gt; &lt;span class="n"&gt;exceptions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Adherence&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NET&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="n"&gt;practices&lt;/span&gt;

&lt;span class="n"&gt;Suggest&lt;/span&gt; &lt;span class="n"&gt;improvements&lt;/span&gt; &lt;span class="n"&gt;but&lt;/span&gt; &lt;span class="n"&gt;don&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="n"&gt;rewrite&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;entire&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Source Generators: The Ultimate Validation Target
&lt;/h3&gt;

&lt;p&gt;This is where the "tools, not decisions" philosophy really shines. Rather than having an agent write your boilerplate code directly, have it write a source generator that produces the boilerplate.&lt;/p&gt;

&lt;p&gt;Consider a common scenario: you have fifty entities that all need repository interfaces, implementations, and registration code. The traditional approach (even with agents) is to generate each one individually, review each one, and hope they're all consistent. The better approach is to have the agent build a source generator that examines your entities and emits the correct code for all of them.&lt;/p&gt;

&lt;p&gt;The prompt might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Create&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="n"&gt;generator&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Finds&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="n"&gt;classes&lt;/span&gt; &lt;span class="n"&gt;decorated&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Generates&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;IXxxRepository&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;with&lt;/span&gt; &lt;span class="n"&gt;standard&lt;/span&gt; &lt;span class="n"&gt;CRUD&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;
&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Generates&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;XxxRepository&lt;/span&gt; &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;our&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
&lt;span class="err"&gt;4.&lt;/span&gt; &lt;span class="nc"&gt;Generates&lt;/span&gt; &lt;span class="n"&gt;extension&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;DI&lt;/span&gt; &lt;span class="n"&gt;registration&lt;/span&gt;

&lt;span class="n"&gt;Conventions&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;follow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Interface&lt;/span&gt; &lt;span class="n"&gt;goes&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Abstractions&lt;/span&gt; &lt;span class="k"&gt;namespace&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="nn"&gt;Implementation&lt;/span&gt; &lt;span class="n"&gt;goes&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Infrastructure&lt;/span&gt; &lt;span class="k"&gt;namespace&lt;/span&gt;
&lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="nn"&gt;Use&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;throughout&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Include&lt;/span&gt; &lt;span class="n"&gt;XML&lt;/span&gt; &lt;span class="n"&gt;documentation&lt;/span&gt;

&lt;span class="n"&gt;Here&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;what&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;generated&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="n"&gt;should&lt;/span&gt; &lt;span class="n"&gt;look&lt;/span&gt; &lt;span class="n"&gt;like&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;desired&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent produces a source generator. You add it to a test project with a few decorated classes. You inspect the generated code. You verify it compiles, follows your conventions, and works correctly. You write tests against the generator itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Generator_Creates_Interface_For_Decorated_Class&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;@"
        [Repository]
        public class Customer { public int Id { get; set; } }
    "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;generated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;RunGenerator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"public interface ICustomerRepository"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Task&amp;lt;Customer?&amp;gt; GetByIdAsync(int id)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Generator_Follows_Namespace_Conventions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;@"
        namespace MyApp.Domain
        {
            [Repository]
            public class Order { }
        }
    "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;generated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;RunGenerator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"namespace MyApp.Abstractions"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"namespace MyApp.Infrastructure"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you've validated the tool. The generator is tested. It produces correct code for your test cases. And when you add your fifty entities, you don't need to review fifty repositories. You need to verify that your entities are properly decorated and let the (validated) generator do its work.&lt;/p&gt;

&lt;p&gt;This is a fundamentally different relationship with AI assistance. You're not trusting the agent to write correct code for each entity. You're trusting yourself to validate a tool that the agent helped you build. The agent's contribution is leverage (building the generator faster than you could), not judgment (deciding what the repositories should look like).&lt;/p&gt;

&lt;p&gt;The same pattern applies to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analyzers&lt;/strong&gt;: Have the agent build Roslyn analyzers that enforce your conventions, rather than trusting it to follow them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaffolding tools&lt;/strong&gt;: Build CLI tools that generate project structures, rather than having the agent create each project&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration generators&lt;/strong&gt;: Build tools that examine schema differences and emit migrations, rather than hand-writing each one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation generators&lt;/strong&gt;: Build tools that extract XML docs and produce markdown, rather than writing docs manually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these transforms "trust the agent's output" into "validate the agent's tool." It's the difference between hiring someone to do a task and hiring someone to build a machine that does the task. The machine can be tested. The machine is consistent. The machine doesn't have bad days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Beyond Scenarios
&lt;/h3&gt;

&lt;p&gt;BDD scenarios define the behavioral contract, but they're not the only tests you need. Implementation-level unit tests, integration tests with real dependencies, and performance tests all have their place.&lt;/p&gt;

&lt;p&gt;For a shared library, I typically want:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario tests&lt;/strong&gt; (BDD): Define the behavioral contract from the consumer's perspective&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unit tests&lt;/strong&gt;: Cover implementation details that scenarios don't reach (edge cases in internal methods, etc.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integration tests&lt;/strong&gt;: Verify behavior against real dependencies (actual HTTP servers, actual circuit breakers under load)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance tests&lt;/strong&gt;: Ensure the implementation doesn't introduce unacceptable overhead&lt;/p&gt;

&lt;p&gt;Agents can generate all of these. The scenario tests exist by definition (you wrote them in the planning phase). Unit tests can be generated from the implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Generate&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;RetryPolicy&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;
&lt;span class="nc"&gt;Achieve&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="m"&gt;90&lt;/span&gt;&lt;span class="p"&gt;%&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="n"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Focus&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;edge&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;zero&lt;/span&gt; &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maximum&lt;/span&gt; &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;invalid&lt;/span&gt; &lt;span class="n"&gt;configurations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Use&lt;/span&gt; &lt;span class="n"&gt;xUnit&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;FluentAssertions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Integration and performance tests usually require more human guidance because they depend on your specific infrastructure, but agents can still do the heavy lifting once you provide the context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Packaging and Publishing
&lt;/h3&gt;

&lt;p&gt;Once the implementation is complete and all tests pass, the library needs to be packaged for consumption. This typically means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NuGet package configuration (.csproj settings, .nuspec if needed)&lt;/li&gt;
&lt;li&gt;Version management (semantic versioning, changelog generation)&lt;/li&gt;
&lt;li&gt;CI/CD pipeline (build, test, pack, publish)&lt;/li&gt;
&lt;li&gt;Package documentation (README, API docs)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of this is boilerplate that agents can generate from templates. The key decisions (version number, release notes, breaking change warnings) require human judgment, but the mechanical work doesn't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;Generate the NuGet package configuration for Company.Http.Resilience.
Include:
- Package metadata (description, authors, license, repository URL)
- Dependency specifications
- README inclusion
- Source Link configuration for debugging

Follow the conventions used in our existing packages:
[paste example .csproj]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Implementation Artifact
&lt;/h3&gt;

&lt;p&gt;The output of the build phase is a functioning library with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Source code&lt;/strong&gt; that implements the specified interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passing scenarios&lt;/strong&gt; that prove behavioral correctness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests&lt;/strong&gt; that cover implementation details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration tests&lt;/strong&gt; that verify real-world behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package configuration&lt;/strong&gt; ready for publishing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt; for consumers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At this point, the concern is extracted. It exists as a standalone, versioned, tested artifact. The remaining work is migrating existing applications to use it and maintaining it going forward.&lt;/p&gt;




&lt;h2&gt;
  
  
  Maintenance and Evolution
&lt;/h2&gt;

&lt;p&gt;This is the phase that everyone forgets about and nobody budgets for. The library is built. The first few applications have migrated. The project is declared a success and the team moves on to the next initiative.&lt;/p&gt;

&lt;p&gt;Six months later, someone files a bug report. A year later, a dependency releases a breaking change. Two years later, a new developer asks why the library does something a certain way and nobody remembers.&lt;/p&gt;

&lt;p&gt;Maintenance is not glamorous, but it's where the investment in good architecture pays dividends (or doesn't).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Maintenance Mindset
&lt;/h3&gt;

&lt;p&gt;The goal of maintenance is not to keep the library frozen in amber. It's to keep it useful, secure, and aligned with the evolving needs of its consumers. This requires:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proactive work&lt;/strong&gt;: Dependency updates, security patches, performance improvements&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reactive work&lt;/strong&gt;: Bug fixes, feature requests, compatibility updates&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Communication&lt;/strong&gt;: Release notes, migration guides, deprecation warnings&lt;/p&gt;

&lt;p&gt;Agents can help with all of these, but they need context about what's changed and what matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency Management
&lt;/h3&gt;

&lt;p&gt;Every library has dependencies, and dependencies evolve. At minimum, you need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track dependency updates (major, minor, patch releases)&lt;/li&gt;
&lt;li&gt;Assess the impact of updates (breaking changes, new features, security fixes)&lt;/li&gt;
&lt;li&gt;Apply updates on a reasonable cadence&lt;/li&gt;
&lt;li&gt;Test thoroughly before releasing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use agents to help with dependency triage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review the following dependency updates for Company.Http.Resilience:

- Microsoft.Extensions.Http: 8.0.0 -&amp;gt; 8.0.1 (patch)
- Polly: 8.2.0 -&amp;gt; 8.3.0 (minor)
- System.Text.Json: 8.0.0 -&amp;gt; 9.0.0 (major)

For each update:
1. Summarize what changed (check release notes)
2. Assess breaking change risk
3. Recommend whether to update now, later, or skip
4. Note any code changes required in our library
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent's assessment isn't final, but it saves hours of manual research. For major version bumps especially, having a summary of breaking changes before diving into the release notes is invaluable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling Feature Requests
&lt;/h3&gt;

&lt;p&gt;Consumers will want things your library doesn't do. This is healthy. It means people are using it. But not every feature request should be implemented.&lt;/p&gt;

&lt;p&gt;For each request, ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is it within scope?&lt;/strong&gt; Does this align with the library's purpose, or should it be a separate concern?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How many consumers need it?&lt;/strong&gt; Is this a common need or a one-off?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can it be done without breaking changes?&lt;/strong&gt; Will existing consumers be affected?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's the maintenance burden?&lt;/strong&gt; Will this make the library harder to understand and maintain?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Agents can help evaluate requests against these criteria:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Evaluate this feature request for Company.Http.Resilience:

Request: Add support for request/response caching

Consider:
- Our stated scope: HTTP client resilience (retry, circuit breaker, timeout)
- Caching is related but distinct from resilience
- Adding caching would increase complexity significantly
- Caching has its own configuration concerns (TTL, invalidation, storage)

Recommendation: Accept, reject, or defer? Justify.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recommendation might be: "Defer. Caching is related to HTTP concerns but orthogonal to resilience. Consider a separate &lt;code&gt;Company.Http.Caching&lt;/code&gt; library that composes with this one. If multiple consumers request it, prioritize the separate library."&lt;/p&gt;

&lt;h3&gt;
  
  
  Version Evolution
&lt;/h3&gt;

&lt;p&gt;Libraries evolve through versions. Semantic versioning provides the framework (major.minor.patch), but the decisions about what goes into each version are human judgment calls.&lt;/p&gt;

&lt;p&gt;I maintain a version roadmap for each shared library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Company.Http.Resilience Version Roadmap&lt;/span&gt;

&lt;span class="gu"&gt;## Current: 1.2.0&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Retry with exponential backoff
&lt;span class="p"&gt;-&lt;/span&gt; Circuit breaker
&lt;span class="p"&gt;-&lt;/span&gt; Configurable timeouts
&lt;span class="p"&gt;-&lt;/span&gt; Structured logging

&lt;span class="gu"&gt;## Planned: 1.3.0 (Q2 2026)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Request hedging (speculative execution)
&lt;span class="p"&gt;-&lt;/span&gt; Enhanced metrics (histograms, percentiles)
&lt;span class="p"&gt;-&lt;/span&gt; .NET 9 support

&lt;span class="gu"&gt;## Planned: 2.0.0 (Q4 2026)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Breaking: Simplified configuration API
&lt;span class="p"&gt;-&lt;/span&gt; Breaking: Remove deprecated methods from 1.x
&lt;span class="p"&gt;-&lt;/span&gt; New: Native AOT support
&lt;span class="p"&gt;-&lt;/span&gt; New: gRPC resilience (separate package)

&lt;span class="gu"&gt;## Backlog (unprioritized)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Request caching (may be separate library)
&lt;span class="p"&gt;-&lt;/span&gt; Custom serializers
&lt;span class="p"&gt;-&lt;/span&gt; Request signing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This roadmap is a living document. It changes as priorities shift and new information emerges. But having it written down prevents the library from drifting aimlessly and helps consumers plan their own upgrades.&lt;/p&gt;

&lt;h3&gt;
  
  
  Documentation Maintenance
&lt;/h3&gt;

&lt;p&gt;Documentation decays. APIs change but examples don't get updated. New features ship without corresponding docs. Deprecated features linger in the guides long after they've been removed.&lt;/p&gt;

&lt;p&gt;Periodic documentation audits are essential:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Audit the documentation for Company.Http.Resilience:

1. Compare public API surface to documented features
2. Identify undocumented public members
3. Identify documented features that no longer exist
4. Check that code examples compile against current version
5. Verify links are not broken

Report discrepancies and suggest fixes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is exactly the kind of tedious, important work that agents excel at. They're patient enough to check every example and thorough enough to catch what humans skip.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Maintenance Rhythm
&lt;/h3&gt;

&lt;p&gt;Rather than treating maintenance as ad-hoc work that happens when things break, establish a rhythm:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weekly&lt;/strong&gt;: Dependency update review (automated alerts, manual triage)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly&lt;/strong&gt;: Feature request review (batch similar requests, make decisions)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quarterly&lt;/strong&gt;: Documentation audit, roadmap review&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yearly&lt;/strong&gt;: Major version planning, breaking change assessment&lt;/p&gt;

&lt;p&gt;This rhythm can be partially automated. Set up alerts for dependency updates. Create recurring calendar items for reviews. Use agents to generate the reports that feed into each review.&lt;/p&gt;

&lt;p&gt;The goal is to make maintenance a sustainable, predictable activity rather than a crisis response.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When I started this journey at my new company, staring down hundreds of codebases and thousands of interwoven concerns, I didn't have a grand plan. A laptop. A curiosity about what AI tools could actually do in practice. An allergy to doing the same thing twice. That's what I had.&lt;/p&gt;

&lt;p&gt;What emerged from that combination is what I've been calling "vibe engineering." Still somewhat tongue-in-cheek, but the name has stuck. It's not a methodology with certification programs and expensive consultants. It's a way of working that recognizes both the immense capability and the critical limitations of AI agents.&lt;/p&gt;

&lt;p&gt;The capability is real. Agents can read code faster than any human. They identify patterns across codebases that would take weeks to discover manually. They generate documentation, draft implementations, produce test suites at a pace that would have seemed like science fiction five years ago.&lt;/p&gt;

&lt;p&gt;The limitations are equally real. Agents don't understand your organization. They don't know why decisions were made. They can't navigate the political landscape that determines what solutions actually get adopted. They lack the judgment to know when "technically correct" isn't good enough.&lt;/p&gt;

&lt;p&gt;Here's the key insight: these capabilities and limitations are complementary.&lt;/p&gt;

&lt;p&gt;Agents excel at exactly the things humans find tedious. Reading vast amounts of code. Maintaining documentation. Generating boilerplate. Checking consistency. Humans excel at exactly the things agents struggle with. Understanding context. Making judgment calls. Navigating organizational dynamics. Designing for the long term.&lt;/p&gt;

&lt;p&gt;Vibe engineering is about creating the right handoff points. Humans define scope, make architectural decisions, review outputs. Agents do the heavy lifting of analysis, generation, and maintenance. The "vibe" is the context, constraints, and creative latitude that make this collaboration productive.&lt;/p&gt;

&lt;p&gt;And part of that vibe is aesthetic.&lt;/p&gt;

&lt;p&gt;I've come to believe that the code style you demand directly impacts the quality of agent collaboration. Flat beats nested. Declarative beats imperative. Fluent beats verbose. Pure functions beat stateful methods. These aren't just preferences. They're force multipliers. Code written in this style is easier to specify, easier to generate, easier to test, and easier to maintain.&lt;/p&gt;

&lt;p&gt;When everything chains cleanly. When each step is a transformation rather than a mutation. When the code reads like a specification of what should happen rather than instructions for how to make it happen. That's when you've created an environment where both humans and agents can do their best work.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I've Learned
&lt;/h3&gt;

&lt;p&gt;Several months into this approach, a few lessons have crystallized.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start small.&lt;/strong&gt; The temptation is to extract everything at once. Build the comprehensive platform SDK that solves all problems. Resist this. Extract one concern at a time. Make it work. Get it adopted. Then move to the next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build tools, not outputs.&lt;/strong&gt; This might be the most important lesson. When you have an agent write code directly, you're trusting its judgment for that specific case. When you have an agent build a source generator, analyzer, or scaffolding tool, you can validate the tool once and trust its output forever. The difference between "agent writes fifty repositories" and "agent writes a generator that produces fifty repositories" is the difference between reviewing fifty files and validating one tool. Always prefer the latter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demand flat, declarative code.&lt;/strong&gt; Agents default to nested, imperative spaghetti. That's what most of their training data looks like. Be explicit about wanting flat, functional, fluent code. Provide examples. The quality difference is enormous. Declarative code is inherently easier to validate because each step is a discrete, testable transformation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invest in specifications.&lt;/strong&gt; The time spent defining interfaces and writing scenarios is not overhead. It's the work. A clear specification makes implementation almost mechanical. Doesn't matter if it's done by an agent, a junior developer, or yourself at 2 AM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust but verify.&lt;/strong&gt; Agent-generated code needs review. Always. Scenarios provide a safety net for behavioral correctness, but style, security, and maintainability require human eyes. This is doubly true for tools and generators, where a bug gets multiplied across every output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design for validation.&lt;/strong&gt; Every interface should be testable. Every behavior should be observable. If you can't write a test that proves the component works correctly, your interface is wrong. This isn't just good practice. It's essential when agents are involved, because validation is how you transform "trust" into "verify."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer fluent APIs.&lt;/strong&gt; Fluent interfaces read like specifications. They're self-documenting. They force separation of concerns. They're easier for both humans and agents to use correctly. When in doubt, make it chainable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintain the paper trail.&lt;/strong&gt; ADRs, inventories, roadmaps, documentation. These aren't bureaucratic overhead. They're institutional memory that keeps work coherent over time. They're also the context that makes future agent interactions productive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Establish rhythms.&lt;/strong&gt; Maintenance is not a crisis response. It's a sustainable practice. Weekly dependency reviews. Monthly feature triage. Quarterly audits. Make it routine and it becomes manageable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bigger Picture
&lt;/h3&gt;

&lt;p&gt;I've focused this essay on cross-cutting concerns because they're the Goldilocks zone for human-agent collaboration. Complex enough to be meaningful. Bounded enough to be tractable. Infrastructure-focused enough that deep business context isn't required.&lt;/p&gt;

&lt;p&gt;But the principles extend beyond infrastructure. Any well-scoped, clearly specified problem can benefit from this approach. The key is knowing when to reach for agent assistance and when to do the work yourself.&lt;/p&gt;

&lt;p&gt;The tools will continue to evolve. Rapidly. The boundary will shift. Tasks requiring heavy human involvement today will become more automatable. New categories of work will emerge that we can't yet imagine. The developers who thrive will be those who can fluidly navigate this shifting boundary, leveraging agents where they're effective and applying human judgment where it's essential.&lt;/p&gt;

&lt;p&gt;For now, though, there's plenty of spaghetti to untangle. Plenty of fungal colonies to dissect. Plenty of cross-cutting concerns hiding in plain sight, waiting to be identified, specified, extracted, and maintained.&lt;/p&gt;

&lt;p&gt;And I've got an agent ready to help.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part one of what I expect to be an ongoing series on AI-assisted software engineering. Future posts will dive deeper into specific techniques, tooling, and real-world case studies. If you want to follow along, the best place is probably my blog at &lt;a href="https://jerrettdavis.com" rel="noopener noreferrer"&gt;jerrettdavis.com&lt;/a&gt; or wherever you found this post.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All code examples in this post are illustrative. The specific implementations would vary based on your stack, constraints, and preferences. The principles, however, are stack-agnostic.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>architecture</category>
      <category>softwaredesign</category>
    </item>
    <item>
      <title>Announcing TinyBDD: Fluent, Executable Scenarios for .NET</title>
      <dc:creator>Jerrett Davis</dc:creator>
      <pubDate>Sat, 27 Dec 2025 19:29:16 +0000</pubDate>
      <link>https://dev.to/jerrettdavis/announcing-tinybdd-fluent-executable-scenarios-for-net-21k4</link>
      <guid>https://dev.to/jerrettdavis/announcing-tinybdd-fluent-executable-scenarios-for-net-21k4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article was originally published on &lt;a href="https://jerrettdavis.com/blog/posts/tinybdd" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Announcing TinyBDD: Fluent, Executable Scenarios for .NET
&lt;/h1&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/jerrettdavis/tinybdd" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.nuget.org/packages/TinyBDD" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt; · &lt;a href="https://jerrettdavis.github.io/TinyBDD/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;What if the shortest path from "we need this" to "it works in prod" was just a single fluent line of code?&lt;/p&gt;

&lt;p&gt;TinyBDD is my attempt to make that path real. It's a lightweight .NET library that lets you write tests in a fluent, Gherkin-ish style—tests that read like acceptance criteria but execute like unit tests. The goal is not ceremony, but clarity: a shared, human-parsable DSL that can span from domain rules to browser automation without losing intent.  &lt;/p&gt;

&lt;p&gt;This post is the practical follow-up to my earlier &lt;a href="https://jerrettdavis.com/blog/posts/making-the-business-write-your-tests-with-bdd" rel="noopener noreferrer"&gt;essay on BDD&lt;/a&gt;. There, I dug into the &lt;em&gt;why&lt;/em&gt;. Here, we'll focus on the &lt;em&gt;how&lt;/em&gt;: how to go from acceptance criteria to running tests in minutes, how to use Given/When/Then to model even the smallest units, how to orchestrate full end-to-end flows with Playwright, and how writing this way naturally nudges your architecture toward SOLID and composable design.&lt;/p&gt;




&lt;h2&gt;
  
  
  From acceptance criteria to running tests in minutes
&lt;/h2&gt;

&lt;p&gt;Every team has seen a story like this written in Jira or Confluence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Scenario: Gold member gets free shipping
Given the customer is a "gold" member
And they have a cart totaling $12.00
When they checkout with standard shipping
Then the shipping cost is $0.00
And the order total is $12.00

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With TinyBDD, you don't need separate &lt;code&gt;.feature&lt;/code&gt; files unless you want them. You can capture that same intent directly in your test framework, keeping the semantics without the tooling overhead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a gold customer with a $12 cart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Cart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Gold&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;12.00m&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"they choose standard shipping"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Shipping&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Standard&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shipping is free"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShippingTotal&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0.00m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;// Pass/Fail with booleans&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order total is $12.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;For&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;12.00m&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// Or Assertions&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertPassed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;The keywords map one-to-one with the business story. Each step is explicit and composable, and the whole chain is easy to read—even for someone outside the dev team. Because the language matches what stakeholders already use, the test itself becomes a living contract.&lt;/p&gt;




&lt;h2&gt;
  
  
  Unit tests that read like behavior
&lt;/h2&gt;

&lt;p&gt;Behavior-driven style isn't just for top-level acceptance tests. It works equally well for small pieces of logic: a pure function, a discount rule, a transformer. By expressing them as Given/When/Then, you get readability—tiny scenarios that explain the intent before diving into implementation detail—and design pressure, because the format gently encourages pure, composable functions.&lt;/p&gt;

&lt;p&gt;Example: a simple discount calculation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;csharp&lt;br&gt;
await Given("a silver customer with $100 cart", () =&amp;gt; (Tier: "silver", Amount: 100m))&lt;br&gt;
     .When("discount is applied", x =&amp;gt; Discounts.Apply(x.Tier, x.Amount))&lt;br&gt;
     .Then("result is $95", result =&amp;gt; result == 95m)&lt;br&gt;
     .AssertPassed();&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Even at this scale, the benefits are obvious. You isolate the decision logic, assert outcomes in plain language, and end up with code that composes neatly into bigger flows later.&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-end UI tests with Playwright
&lt;/h2&gt;

&lt;p&gt;TinyBDD also works at the other end of the spectrum: full-stack, end-to-end tests. Here, the key is keeping steps thin and expressive while pushing implementation detail into helpers like page objects or service wrappers. That way, the scenario text stays stable even if the UI shifts underneath.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;csharp&lt;br&gt;
await Given("a new browser and logged-in gold user", async () =&amp;gt;&lt;br&gt;
{&lt;br&gt;
    var pw = await PlaywrightFactory.LaunchAsync();&lt;br&gt;
    var page = await pw.NewPageAsync();&lt;br&gt;
    await AuthSteps.LoginAsGoldAsync(page);&lt;br&gt;
    return page;&lt;br&gt;
})&lt;br&gt;
.When("user adds a $12 item to cart", async page =&amp;gt;&lt;br&gt;
{&lt;br&gt;
    await CatalogSteps.AddItemAsync(page, "SKU-123", 12.00m);&lt;br&gt;
    return page;&lt;br&gt;
})&lt;br&gt;
.And("proceeds to checkout with standard shipping", CheckoutSteps.StandardAsync)&lt;br&gt;
.Then("shipping is free", async page =&amp;gt;&lt;br&gt;
{&lt;br&gt;
    var shipping = await CartSteps.ReadShippingAsync(page);&lt;br&gt;
    return shipping == 0.00m;&lt;br&gt;
})&lt;br&gt;
.And("order total is $12.00", async page =&amp;gt;&lt;br&gt;
{&lt;br&gt;
    var total = await CartSteps.ReadTotalAsync(page);&lt;br&gt;
    return total == 12.00m;&lt;br&gt;
})&lt;br&gt;
.AssertPassed();&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Scenarios like this are readable enough for a stakeholder to skim, while still giving engineers the control they need under the hood. Stable wording, deterministic helpers, and tagging (&lt;code&gt;smoke&lt;/code&gt;, &lt;code&gt;ui&lt;/code&gt;, &lt;code&gt;checkout&lt;/code&gt;) all contribute to making suites like this maintainable in real CI pipelines.&lt;/p&gt;




&lt;h3&gt;
  
  
  Patterns that keep this maintainable
&lt;/h3&gt;

&lt;p&gt;The trick to making end-to-end scenarios sustainable is resisting the temptation to let your steps do all the heavy lifting. The step chain should stay thin and intention-revealing, while the real mechanics live in helpers: page objects, domain services, or test utilities. This keeps the scenario text stable even as the implementation evolves. A good rule of thumb is that a non-technical stakeholder should be able to scan the steps and nod along without ever seeing the helper code. Deterministic helpers—free from hidden global state—are key to repeatable results. And once you have a handful of scenarios, you'll want to tag them (&lt;code&gt;smoke&lt;/code&gt;, &lt;code&gt;ui&lt;/code&gt;, &lt;code&gt;checkout&lt;/code&gt;, etc.) so that CI pipelines can run fast slices for quick feedback and broader sweeps when confidence matters most.&lt;/p&gt;




&lt;h3&gt;
  
  
  Let tests guide your design
&lt;/h3&gt;

&lt;p&gt;When you write tests in a behavior-first style, architectural friction surfaces quickly. A step that requires half a dozen parameters is rarely a coincidence—it usually means your modules are too tightly coupled. Repeating the same tedious setup across multiple scenarios suggests the absence of a proper abstraction. And if you struggle to phrase a step cleanly, the problem may not be the test at all, but the clarity of your domain language.&lt;/p&gt;

&lt;p&gt;These moments of friction are signals. Often, the fix is to extract a pure function from a messy edge, create a port or adapter to decouple infrastructure from business rules, or split a workflow into smaller seams that deserve their own scenarios. In other words: the pressure you feel in writing the test is your design telling you what it wants to become.&lt;/p&gt;




&lt;h3&gt;
  
  
  BDD and the drift toward SOLID and functional design
&lt;/h3&gt;

&lt;p&gt;Consistently writing scenarios has a shaping effect on code. Steps that do one clear thing align with the Single Responsibility Principle. The ability to add new scenarios without editing existing ones echoes the Open/Closed Principle. And abstractions that are narrow, well-defined, and swappable make substituting fakes and stubs trivial, pushing you toward Liskov, ISP, and DIP almost by default.&lt;/p&gt;

&lt;p&gt;The same is true for functional composition. Pure functions naturally slide into Given/When/Then flows. Side effects are easiest to reason about when pushed to the edges—fetching in a Given, transforming in a When, and observing in a Then. And when steps are small and named, they read like a pipeline instead of a mess of conditionals. By following the test style, you often find yourself following the design style too.&lt;/p&gt;




&lt;h3&gt;
  
  
  A practical way to start tomorrow
&lt;/h3&gt;

&lt;p&gt;If this feels overwhelming, don't boil the ocean. Start with one slice of functionality that everyone values and recognizes—maybe a login path or a simple checkout. Write just two or three scenarios, and make sure the wording mirrors how the business describes the flow. Delegate the mechanics to helpers, not the scenario text. Keep your domain logic in pure functions wherever possible so it's trivial to call from a &lt;code&gt;When&lt;/code&gt; step. And once you've got a couple of green runs, wire in some tags so you can choose between smoke tests, integration runs, or the full suite depending on your CI needs.&lt;/p&gt;

&lt;p&gt;As you go, pay attention to the words. If step text feels clumsy, it probably means your ubiquitous language is clumsy too. Refining that wording in collaboration with stakeholders isn't overhead—it's the work. And when naming friction crops up, it's often a smell that your design needs another seam or abstraction.&lt;/p&gt;




&lt;h3&gt;
  
  
  Fluent examples you can copy-paste
&lt;/h3&gt;

&lt;p&gt;Unit-level:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;csharp&lt;br&gt;
await Given("subtotal is $120 and tier is gold", () =&amp;gt; (Subtotal: 120m, Tier: "gold"))&lt;br&gt;
     .When("finalize price", x =&amp;gt; Pricing.Finalize(x.Tier, x.Subtotal))&lt;br&gt;
     .Then("applies 10% discount", price =&amp;gt; price == 108m)&lt;br&gt;
     .AssertPassed();&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;API-level:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;csharp&lt;br&gt;
await Given("a seeded test tenant", TestData.SeedTenantAsync)&lt;br&gt;
     .When("posting to /invoices", async _ =&amp;gt; await Api.PostAsync("/invoices", new { amount = 250 }))&lt;br&gt;
     .Then("returns 201", r =&amp;gt; r.StatusCode == 201)&lt;br&gt;
     .And("body contains invoice id", r =&amp;gt; r.Json.Value&amp;lt;string&amp;gt;("id") is { Length: &amp;gt; 0 })&lt;br&gt;
     .AssertPassed();&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;UI-level:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;csharp&lt;br&gt;
await Given("a logged-in admin", BrowserSteps.LoginAsAdminAsync)&lt;br&gt;
     .When("they create a user named Dana", page =&amp;gt; AdminUsers.CreateAsync(page, "Dana"))&lt;br&gt;
     .Then("Dana appears in the grid", page =&amp;gt; AdminUsers.ExistsAsync(page, "Dana"))&lt;br&gt;
     .AssertPassed();&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Readable Gherkin-style output
&lt;/h3&gt;

&lt;p&gt;One of the nicest touches in TinyBDD is how your scenarios &lt;em&gt;report themselves&lt;/em&gt; when you run the tests. Pair your scenarios with the appropriate base class (&lt;code&gt;TinyBddXunitBase&lt;/code&gt;, &lt;code&gt;TinyBddXunitV3Base&lt;/code&gt;, &lt;code&gt;TinyBddNUnitBase&lt;/code&gt;, or &lt;code&gt;TinyBddMSTestBase&lt;/code&gt;), and the test runner will print structured Gherkin output alongside normal results.  &lt;/p&gt;

&lt;p&gt;That means the Given/When/Then flow you wrote doesn't just execute—it shows up exactly as you'd expect, step by step, with timings and pass/fail indicators. It turns your test logs into living specifications.&lt;/p&gt;

&lt;p&gt;For example, here's the output from a mediator scenario:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`&lt;/p&gt;

&lt;p&gt;Feature: Behavioral - Mediator (commands, notifications, streaming, behaviors)&lt;br&gt;
Scenario: Send: command handler runs through behaviors and returns value&lt;br&gt;
Given a mediator with pre/post/whole behaviors and a Ping-&amp;gt;Pong handler [OK] 2 ms&lt;br&gt;
When sending Ping(5) [OK] 4 ms&lt;br&gt;
Then result is pong:5 [OK] 2 ms&lt;br&gt;
And behaviors logged pre, whole before/after, and post [OK] 0 ms&lt;/p&gt;

&lt;p&gt;`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Instead of squinting at assertions in code, you see a natural-language story of what happened. That's invaluable when sharing results with stakeholders or debugging failures in CI. And because the feature and scenario titles come from your test class and attributes, the logs stay consistent with the language you use in code reviews, planning, and conversations.&lt;/p&gt;




&lt;h3&gt;
  
  
  Avoiding common anti-patterns
&lt;/h3&gt;

&lt;p&gt;Every test framework accumulates bad habits if left unchecked, and TinyBDD is no exception. The most obvious trap is clever wording: steps like "When magic happens" don't help anyone and fail to serve as documentation. Instead, the wording should describe an intention that a stakeholder would immediately recognize, such as "When the admin disables the account". Another trap is letting a single step conceal multiple actions or checks. Keep your flow honest: &lt;code&gt;When&lt;/code&gt; should drive effects, and &lt;code&gt;Then&lt;/code&gt; or &lt;code&gt;And&lt;/code&gt; should assert results.&lt;/p&gt;

&lt;p&gt;Setup is another danger zone. If your &lt;code&gt;Given&lt;/code&gt; steps are littered with manual wiring of objects, it's time for factories or builders to take over. And at the UI layer, brittle selectors quickly make tests flaky; encapsulating them in page objects and using explicit test IDs pays off many times over. Avoiding these pitfalls keeps your suite readable, stable, and genuinely valuable.&lt;/p&gt;




&lt;h3&gt;
  
  
  The payoff
&lt;/h3&gt;

&lt;p&gt;When scenarios read like the business and execute like the code, something special happens. Your tests stop being just a safety net and start becoming living documentation. They never go stale because they're executable. They provide immediate feedback on drift, so change becomes safer. They subtly nudge your codebase toward SOLID principles and functional seams. And for new developers, they become the best possible onboarding guide: open the suite, read the stories, and understand how the system behaves.&lt;/p&gt;

&lt;p&gt;You don't need to retool your world to reach this point. Start with one scenario. Make it pass. Share it with your team. Repeat. In a few sprints, you'll have a suite of stories that stack from units to workflows, and a codebase that's easier to evolve because the behaviors are crystal clear.&lt;/p&gt;




&lt;h3&gt;
  
  
  Appendix — A quick PR lens
&lt;/h3&gt;

&lt;p&gt;As you review changes, ask yourself: does this PR add or update scenarios that the business would recognize? Do the steps read like natural English, each mapping to a single intent? Are the domain rules isolated in pure functions rather than tangled in infrastructure? Did we create or clarify a port instead of hard-coding dependencies? Can we tag and run this slice of scenarios independently in CI?&lt;/p&gt;

&lt;p&gt;If you can answer "yes" to most of those, you're not just writing tests—you're building shared understanding, guiding design, and accelerating delivery. That's the real promise of TinyBDD.&lt;/p&gt;




&lt;p&gt;👉 &lt;a href="https://github.com/jerrettdavis/tinybdd" rel="noopener noreferrer"&gt;Get TinyBDD on GitHub&lt;/a&gt; · &lt;a href="https://www.nuget.org/packages/TinyBDD" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt; · &lt;a href="https://jerrettdavis.github.io/TinyBDD/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>bdd</category>
      <category>tdd</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>JD.Efcpt.Build: Build‑Time EF Core Scaffolding to Keep Database‑First Models in Sync</title>
      <dc:creator>Jerrett Davis</dc:creator>
      <pubDate>Mon, 22 Dec 2025 05:43:45 +0000</pubDate>
      <link>https://dev.to/jerrettdavis/jdefcptbuild-build-time-ef-core-scaffolding-to-keep-database-first-models-in-sync-5b5n</link>
      <guid>https://dev.to/jerrettdavis/jdefcptbuild-build-time-ef-core-scaffolding-to-keep-database-first-models-in-sync-5b5n</guid>
      <description>&lt;h1&gt;
  
  
  "Where Did Database First Go?"
&lt;/h1&gt;

&lt;p&gt;If you were using Entity Framework when EF Core first dropped, you probably remember the moment you went looking for database-first support and found... nothing.&lt;/p&gt;

&lt;p&gt;EF Core launched as a code-first framework. The Reverse Engineer tooling that EF6 developers relied on (the right-click, point at a database, generate your models workflow) wasn't there. Microsoft's position was essentially "migrations are the future, figure it out." And if your team had an existing database, or a DBA who actually owned the schema, or compliance requirements that meant the database was the source of truth... well, good luck with that.&lt;/p&gt;

&lt;p&gt;The community's response was immediate and loud. "Where did database first go?" became a recurring theme in GitHub issues, Stack Overflow questions, and the quiet frustration of developers who just wanted to talk to their database without hand-writing a hundred entity classes.&lt;/p&gt;

&lt;p&gt;Eventually, tooling caught up. EF Core Power Tools emerged as the community answer: a Visual Studio extension that brought back the reverse engineering workflow. You could point it at a database or a DACPAC, configure some options, and generate your models. Problem solved, mostly.&lt;/p&gt;

&lt;p&gt;But here's the thing about manual processes: they work fine right up until they don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem That Keeps Happening
&lt;/h2&gt;

&lt;p&gt;I've spent enough time in codebases with legacy data layers to recognize a pattern. It goes something like this:&lt;/p&gt;

&lt;p&gt;A project starts with good intentions. Someone sets up EF Core Power Tools, generates the initial models, commits everything, and documents the process. "When the schema changes, regenerate the models using this tool with these settings." Clear enough.&lt;/p&gt;

&lt;p&gt;Then time passes.&lt;/p&gt;

&lt;p&gt;The developer who set it up leaves. The documentation gets stale. Someone regenerates with slightly different settings and commits the result. Someone else forgets to regenerate entirely after a schema change. The models drift. The configuration drifts. Nobody's quite sure what the "correct" regeneration process is anymore, so people just... stop doing it consistently.&lt;/p&gt;

&lt;p&gt;This isn't a dramatic failure. It's a slow erosion. The kind of problem that doesn't announce itself until you're debugging a production issue and realize the entity class doesn't have a column that's been in the database for six months.&lt;/p&gt;

&lt;p&gt;If you've worked in a codebase long enough, you've probably seen some version of this. Maybe you've been the person who discovered the drift. Maybe you've been the person who caused it. (No judgment. We've all been there.)&lt;/p&gt;

&lt;p&gt;The frustrating part is that the fix is always the same: regenerate the models, commit the changes, remind everyone to regenerate after schema changes. And then six months later, you're having the same conversation again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Manual Regeneration Fails
&lt;/h2&gt;

&lt;p&gt;Let's be specific about what goes wrong, because understanding the failure modes is the first step toward fixing them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ownership problem.&lt;/strong&gt; Whose job is it to regenerate after a schema change? The person who changed the schema? The person who owns the data layer? The tech lead? Nobody has a clear answer, which means sometimes everyone does it (chaos) and sometimes nobody does it (drift).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The configuration problem.&lt;/strong&gt; EF Core Power Tools stores settings in JSON files. Namespaces, nullable reference types, navigation property generation, renaming rules. There are dozens of options. If developers regenerate with different configurations, you get inconsistent output. Same database, different generated code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tooling problem.&lt;/strong&gt; Regeneration requires Visual Studio with the extension installed. CI servers don't have Visual Studio. New developers might not have the extension. Remote development setups might not support it. The process that works on one machine doesn't necessarily work on another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The noise problem.&lt;/strong&gt; Regeneration often produces massive diffs. Property reordering, whitespace changes, attribute additions. Stuff that doesn't represent actual schema changes but clutters up the commit. Developers learn to distrust regeneration diffs, which makes them reluctant to regenerate, which makes the problem worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The timing problem.&lt;/strong&gt; Even when everyone knows the process, there's no enforcement. You can commit code that references a column the models don't have, and the build might still pass if nothing actually uses that code path yet. The error surfaces later, in a different context, when the connection to the original schema change is long forgotten.&lt;/p&gt;

&lt;p&gt;None of these are individually catastrophic. Together, they add up to a process that works in theory but fails in practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;Here's the thought that eventually became this project: if model generation can be invoked from the command line (and it can, via EF Core Power Tools CLI), then model generation can be part of the build.&lt;/p&gt;

&lt;p&gt;Not a separate step you remember to run. Not a manual process with unclear ownership. Just part of what happens when you run &lt;code&gt;dotnet build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The build already knows how to compile your code. It already knows how to restore packages, run analyzers, produce artifacts. Adding "generate EF Core models from the schema" to that list isn't conceptually different from any other build-time code generation.&lt;/p&gt;

&lt;p&gt;If the build handles it, the ownership question disappears. The build owns it. If the build handles it with consistent configuration, the drift disappears. Everyone gets the same output. If the build handles it on every machine, the tooling problem disappears. No special extensions required.&lt;/p&gt;

&lt;p&gt;This is JD.Efcpt.Build: an MSBuild integration that makes EF Core model generation automatic.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Actually Works
&lt;/h2&gt;

&lt;p&gt;The package hooks into your build through MSBuild targets that run before compilation. When you build, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Finds your schema source.&lt;/strong&gt; Either a SQL Server Database Project (&lt;code&gt;.sqlproj&lt;/code&gt;) that gets compiled to a DACPAC, or a connection string pointing to a live database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Computes a fingerprint.&lt;/strong&gt; A hash of all the inputs: the DACPAC or schema metadata, the configuration file, the renaming rules, any custom templates. This fingerprint represents "the current state of everything that affects generation."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compares to the previous fingerprint.&lt;/strong&gt; If they match, nothing changed, and generation is skipped. If they differ, something changed, and generation runs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generates models.&lt;/strong&gt; Using EF Core Power Tools CLI, same as you'd run manually, but automated. Output goes to &lt;code&gt;obj/efcpt/Generated/&lt;/code&gt; with a &lt;code&gt;.g.cs&lt;/code&gt; extension.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Adds generated files to compilation.&lt;/strong&gt; Automatically. You don't edit your project file or manage includes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fingerprinting is what makes this practical. You don't want generation running on every build. That would be slow and developers would hate it. The fingerprint check is fast (XxHash64, designed for exactly this kind of content comparison), so incremental builds have essentially zero overhead. Generation only runs when inputs actually change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Ways to Get Your Schema
&lt;/h2&gt;

&lt;p&gt;Different teams manage database schemas differently, so the package supports two modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DACPAC Mode&lt;/strong&gt; is for teams with SQL Server Database Projects. You have a &lt;code&gt;.sqlproj&lt;/code&gt; that defines your schema in version-controlled SQL files. The package builds this project to produce a DACPAC, then generates models from that DACPAC.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;EfcptSqlProj&amp;gt;&lt;/span&gt;..\Database\MyDatabase.sqlproj&lt;span class="nt"&gt;&amp;lt;/EfcptSqlProj&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is nice because your schema is code. It lives in source control. Changes go through pull requests. The DACPAC is a build artifact, and models are derived from that artifact deterministically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection String Mode&lt;/strong&gt; is for teams without database projects. Maybe you apply migrations to a dev database and want to scaffold from that. Maybe you're working against a cloud database. Maybe you just don't want to deal with DACPACs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;EfcptConnectionString&amp;gt;&lt;/span&gt;$(DB_CONNECTION_STRING)&lt;span class="nt"&gt;&amp;lt;/EfcptConnectionString&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package connects, queries system tables to understand the schema, and generates from that. The fingerprint is computed from the schema metadata, so incremental builds still work. If the schema hasn't changed, generation is skipped.&lt;/p&gt;

&lt;p&gt;Both modes use the same configuration files and produce the same kind of output. They just differ in where the schema comes from.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting It Up
&lt;/h2&gt;

&lt;p&gt;The minimum setup is almost trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"JD.Efcpt.Build"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.0.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have a &lt;code&gt;.sqlproj&lt;/code&gt; in your solution and an &lt;code&gt;efcpt-config.json&lt;/code&gt; in your project directory, that's it. Run &lt;code&gt;dotnet build&lt;/code&gt; and models appear.&lt;/p&gt;

&lt;p&gt;For more control, you add configuration. The &lt;code&gt;efcpt-config.json&lt;/code&gt; controls generation behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"names"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"root-namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyApp.Data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dbcontext-name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ApplicationDbContext"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code-generation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"use-nullable-reference-types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enable-on-configuring"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;enable-on-configuring: false&lt;/code&gt; means your DbContext won't have a hardcoded connection string. You configure that in your DI container, where it belongs.&lt;/p&gt;

&lt;p&gt;If your database uses naming conventions that don't map cleanly to C#, you add renaming rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SchemaName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dbo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Tables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tbl_Users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"NewName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Columns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"NewName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Id"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;tbl_Users.user_id&lt;/code&gt; becomes &lt;code&gt;User.Id&lt;/code&gt;. The database can keep its conventions, your C# code can have its conventions, and the mapping is explicit and version-controlled.&lt;/p&gt;




&lt;h2&gt;
  
  
  What About Custom Code?
&lt;/h2&gt;

&lt;p&gt;A reasonable concern: "I have computed properties and validation methods on my entities. Won't regeneration overwrite those?"&lt;/p&gt;

&lt;p&gt;This is what partial classes are for.&lt;/p&gt;

&lt;p&gt;The generated entity is one half:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// obj/efcpt/Generated/User.g.cs&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FirstName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LastName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your custom logic is the other half:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Models/User.cs&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FullName&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;FirstName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;LastName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;HasValidEmail&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both compile into a single class. The generated half gets regenerated every build. Your custom half stays exactly as you wrote it.&lt;/p&gt;

&lt;p&gt;This separation is actually cleaner than mixing generated and custom code in the same file. You know at a glance what's generated (&lt;code&gt;.g.cs&lt;/code&gt; in &lt;code&gt;obj/&lt;/code&gt;) and what's yours (everything else).&lt;/p&gt;




&lt;h2&gt;
  
  
  CI/CD Without Special Steps
&lt;/h2&gt;

&lt;p&gt;One of the pain points I mentioned earlier was CI/CD. Manual regeneration doesn't work in automated pipelines. You're stuck either committing generated code (merge conflicts) or maintaining custom regeneration scripts (fragile).&lt;/p&gt;

&lt;p&gt;With the build handling generation, CI just works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup .NET&lt;/span&gt;
    &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-dotnet@v4&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;dotnet-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10.0.x'&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet build --configuration Release&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_CONNECTION_STRING&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DB_CONNECTION_STRING }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No special steps for EF Core generation. The build handles it. On .NET 10+, the package uses &lt;code&gt;dotnet dnx&lt;/code&gt; to execute the tool directly from the package feed without requiring installation. On older versions, it uses tool manifests or global tools.&lt;/p&gt;

&lt;p&gt;Pull requests that include schema changes automatically include the corresponding model changes, because both happen during the build. Schema and code are validated together.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Things Go Wrong
&lt;/h2&gt;

&lt;p&gt;Things will go wrong. Here's how you figure out what happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable verbose logging:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;EfcptLogVerbosity&amp;gt;&lt;/span&gt;detailed&lt;span class="nt"&gt;&amp;lt;/EfcptLogVerbosity&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build output now includes exactly what's happening: which inputs were found, what fingerprint was computed, whether generation ran or was skipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the resolved inputs:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After a build, look at &lt;code&gt;obj/efcpt/resolved-inputs.json&lt;/code&gt;. This shows exactly what the package found for each input. If something's wrong, you'll see it here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inspect the fingerprint:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fingerprint is stored at &lt;code&gt;obj/efcpt/fingerprint.txt&lt;/code&gt;. If generation is running unexpectedly (or not running when it should), the fingerprint tells you whether inputs changed from the package's perspective.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who This Is For
&lt;/h2&gt;

&lt;p&gt;I want to be honest about fit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is probably for you if:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You're doing database-first development and you've experienced the regeneration coordination problem. The "did someone regenerate?" question has come up, and the answer wasn't always clear.&lt;/p&gt;

&lt;p&gt;Your schema changes regularly. If you're shipping schema changes weekly, manual regeneration becomes friction.&lt;/p&gt;

&lt;p&gt;You want builds that work identically everywhere. Local machines, CI servers, new developer laptops. Everyone should get the same generated code from the same inputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This probably isn't for you if:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your schema is essentially static. If schema changes are rare, manual regeneration isn't that painful.&lt;/p&gt;

&lt;p&gt;You're using code-first migrations. If migrations are your source of truth, you're solving a different problem.&lt;/p&gt;

&lt;p&gt;You're not using EF Core Power Tools already. This package automates EF Core Power Tools; if you're using a different generation approach, this doesn't apply.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Groan, Addressed
&lt;/h2&gt;

&lt;p&gt;"Where did database first go?"&lt;/p&gt;

&lt;p&gt;It's been years since EF Core launched without reverse engineering, and the tooling has caught up. EF Core Power Tools exists. The CLI exists. The capability is there.&lt;/p&gt;

&lt;p&gt;But capability isn't the same as workflow. Having the tools isn't the same as having a process that works reliably across a team, across time, across environments.&lt;/p&gt;

&lt;p&gt;JD.Efcpt.Build is an attempt to close that gap. To take the capability that exists and make it automatic. To make the build the owner of model generation, so humans don't have to remember to do it.&lt;/p&gt;

&lt;p&gt;Your database schema is the source of truth. This package just makes sure your code reflects that truth, every time you build, without manual intervention.&lt;/p&gt;

&lt;p&gt;One less thing to coordinate. One less thing to forget. One less thing to go wrong in production because a manual step got skipped.&lt;/p&gt;

&lt;p&gt;That's the pitch. Give it a try if it fits your situation.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;JD.Efcpt.Build is &lt;a href="https://github.com/JerrettDavis/JD.Efcpt.Build" rel="noopener noreferrer"&gt;open source&lt;/a&gt; and available on &lt;a href="https://www.nuget.org/packages/JD.Efcpt.Build" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>database</category>
      <category>automation</category>
      <category>programming</category>
    </item>
    <item>
      <title>You Don't Hate Abstractions</title>
      <dc:creator>Jerrett Davis</dc:creator>
      <pubDate>Mon, 08 Dec 2025 01:08:42 +0000</pubDate>
      <link>https://dev.to/jerrettdavis/you-dont-hate-abstractions-3gl8</link>
      <guid>https://dev.to/jerrettdavis/you-dont-hate-abstractions-3gl8</guid>
      <description>&lt;p&gt;It’s an hour until you’re free for the weekend, and you’re trying to knock out one&lt;br&gt;
last ticket before you escape into whatever assuredly action-packed plans await you.&lt;br&gt;
You spot a seemingly harmless task: "Add Middle Initial to User Name Display."&lt;/p&gt;

&lt;p&gt;You chuckle. Easy. A palate cleanser. A victory lap.&lt;br&gt;&lt;br&gt;
You assign the ticket, flip it from &lt;code&gt;New&lt;/code&gt; to &lt;code&gt;Active&lt;/code&gt;, and let your IDE warm up&lt;br&gt;&lt;br&gt;
while you drift into a pleasant daydream about not being here.&lt;/p&gt;

&lt;p&gt;But then the search results begin to appear.&lt;br&gt;&lt;br&gt;
Slowly. Line by line.&lt;/p&gt;

&lt;p&gt;And your reverie begins to rot.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;IUserNameStrategy&lt;/code&gt;, &lt;code&gt;UserNameContext&lt;/code&gt;, &lt;code&gt;UserNameDisplayStrategyFactory&lt;/code&gt;,&lt;br&gt;&lt;br&gt;
&lt;code&gt;StandardUserNameDisplayStrategy&lt;/code&gt;, &lt;code&gt;FormalUserNameDisplayStrategy&lt;/code&gt;,&lt;br&gt;&lt;br&gt;
&lt;code&gt;InformalUserNameDisplayStrategy&lt;/code&gt;, &lt;code&gt;UserNameDisplayModule&lt;/code&gt;, …&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Incredulous, your chuckle hardens into a throat-scraping noise somewhere&lt;br&gt;
between a laugh and a cry.&lt;/p&gt;

&lt;p&gt;"What in the Gang-of-Fuck is happening here," you think, feeling your pulse tick up.&lt;br&gt;
"Either someone read &lt;a href="https://refactoring.guru" rel="noopener noreferrer"&gt;Refactoring.Guru&lt;/a&gt; like it was scripture and decided to&lt;br&gt;
baptize the codebase in every pattern they learned, or a grizzled enterprise&lt;br&gt;
veteran escaped some Netflix-adjacent monolith and is trying to upskill in&lt;br&gt;
TypeScript. Because surely no sane developer would build this much redirection&lt;br&gt;
for such a trivial feature… right?"&lt;/p&gt;

&lt;p&gt;That tiny, spiraling task is a perfect microcosm of a continuous debate across engineering circles: when does abstraction help, and when does it become a hindrance?&lt;/p&gt;



&lt;p&gt;I recently stumbled across the humorous article &lt;a href="https://dev.to/adamthedeveloper/youre-not-building-netflix-stop-coding-like-you-are-1707?ref=dailydev"&gt;You're Not Building Netflix: Stop Coding Like You Are&lt;/a&gt; by Adam — The Developer. But though it resonates in many ways, its broader critique is ultimately misdirected.&lt;/p&gt;

&lt;p&gt;Adam opens with a more complete version of the code mocked in my prologue, and uses the verbosity and obscurity of that abstraction pile as the springboard for a rebuke of using enterprise patterns pretty much across the board. While the author does allow for some abstractions, they're limited in application and scope.&lt;/p&gt;

&lt;p&gt;The problem isn’t that the complaint is wrong. It’s that it points the finger at the wrong culprit, just about literally missing the forest for the trees.&lt;/p&gt;

&lt;p&gt;Abstractions are fundamental and essential. They are the elementary particles of software, the quarks and leptons that bind into the subatomic structures that become the atoms our earliest techno-wizards fused into molecules. Today we combine those same basic elements into the compounds and contraptions made for use by millions. Without abstractions, we are left helpless in the ever-increasing parallel streams of pulsating electrical currents, rushing through specialized, intricately forged rocks that artisan wizards once trapped lightning inside and somehow convinced to think.&lt;/p&gt;

&lt;p&gt;But even with all that power at our disposal, the way we use these building blocks matters. Chemistry offers a fitting parallel. Food chemists, for example, have spent decades learning how to repurpose industrial byproducts into stabilizers, textures, preservatives, and anything else that can be quietly slipped into a recipe. Much of this work is impressive and innovative, but some of it is little more than creative waste disposal disguised as convenience: a brilliant hack in the short term and a lingering problem in the long one.&lt;/p&gt;

&lt;p&gt;Developers can fall into the same pattern. We learn a new technique or pattern or clever trick and then spend the next year pouring it into every beaker we can find. We are not always discovering better processes. Sometimes we are just repackaging the same product and calling it progress. When that happens, we are not solving problems. We are manufacturing new ones that future maintainers will curse our names over.&lt;/p&gt;

&lt;p&gt;A developer must be architect, engineer, mechanic, and driver all at once. It is fine to know how to fix a specific issue, but once that problem is solved, that knowledge should become a building block for solving the next one. If we keep returning to maintain the same solution day after day, then what we built was never a solution at all. It was a slow-burning maintenance burden that we misfiled as "done."&lt;/p&gt;

&lt;p&gt;Abstractions exist to reduce complexity, not to multiply it. Their purpose is to lighten the cognitive load, to lift the details off your desk so you can see the shape of the problem in front of you. Terse, repetitive, wire-on-the-floor code that looks like it tumbled out of a &lt;a href="https://youtu.be/MvEXkd3O2ow" rel="noopener noreferrer"&gt;flickering green CRT from 1999 may impress the authors who have stared at machine code long enough to discern hair color from a data stream&lt;/a&gt;, but it does not serve the broader team or the system that outlives them. Abstractions only do their job when they are aligned with the actual problem being solved, and that brings us to the part many developers skip entirely: modeling your software after the problem you are solving.&lt;/p&gt;
&lt;h2&gt;
  
  
  Seeing the Problem Before Solving It
&lt;/h2&gt;

&lt;p&gt;When you build a system, any system, even a disposable script, the first responsibility is understanding why it exists. What problem does it address. Has that problem been solved before. If so, what makes the existing solution insufficient for you now. Understanding that difference is the foundation that everything else must sit on.&lt;/p&gt;

&lt;p&gt;I learned this the hard way as a homeowner. My house is old enough to have grounded me if I talked back to it as a teenager. A couple of years ago we went through a near-total remodel. We did some work before and shortly after our daughter was born, but within a year new problems started surfacing. We brought in a structural engineer. The slab foundation was heaving. After some exploration we discovered the culprit: the original cast iron sewage line had split along both the top and bottom, creating pressure changes and settling issues throughout the house.&lt;/p&gt;

&lt;p&gt;The fix was not small. We pulled up every inch of flooring. Replaced baseboards. Repaired drywall. Fixed the broken line. Repainted entire sections. Redid trim. Installed piers. Pumped in foundation foam. Cashed in favors. Lost many weekends. And yet, even with all that, it still cost far less than buying an equivalent house in the current market at the current rates.&lt;/p&gt;

&lt;p&gt;The lesson is simple. Things are rarely a total loss. Even when a structure looks hopeless, even when someone has effectively set fire to best practices, even when regulations or markets or technologies have shifted dramatically, there are almost always assets worth salvaging inside the wreckage. You should not bulldoze unless you know you have truly exhausted the alternatives.&lt;/p&gt;

&lt;p&gt;Before throwing away any system and starting another from scratch, assess what you already have. Understand what is broken, what is sound, and what simply needs reinforcement. Software, like houses, tends to rot in specific places for specific reasons. Understanding those reasons is what separates renovation from reinvention.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Nightstand Problem
&lt;/h2&gt;

&lt;p&gt;The same principle applies at a smaller scale. You may already own a perfectly functional house with perfectly functional furniture, yet still want a nightstand you do not currently possess. Your choices are straightforward. You can hope someone has decided to let go of one that happens to meet your criteria. That is the open source gamble. You can buy one, constrained only by budget and whatever definition of quality the manufacturer is committed to that week. Or you can build one yourself, limited only by your skills, imagination, and tolerance for sawdust.&lt;/p&gt;

&lt;p&gt;If your goal is personal satisfaction or experimentation, then by all means build the nightstand. But if your goal is to sell or support a product that helps make money, you are no longer just hobby-carpenting. You are operating in the domain of enterprise software.&lt;/p&gt;

&lt;p&gt;And when you are building enterprise software, you must view the system from the top down while designing from the bottom up. From the top down, you think about every consumer of your system. In academic terms these are actors. Any system intended to be used, whether by humans or machines, is defined by the interactions between its actors and its responsibilities. Even an autonomous system is both an actor and the architected environment it operates within.&lt;/p&gt;

&lt;p&gt;This perspective matters because it forces your abstractions to model the real world rather than some internal taxonomy of clever names. Good abstractions emerge from an understanding of the domain. Bad abstractions emerge from an understanding of a design pattern book.&lt;/p&gt;

&lt;p&gt;And if you want maintainability, clarity, and longevity, you always want the first.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building from Both Directions
&lt;/h2&gt;

&lt;p&gt;Designing software means working from two directions at once. On one hand, you must understand the behavior your system must exhibit. On the other hand, you must understand the shape of the world it lives in. Systems are not invented whole cloth; they crystallize out of the interactions between intentions and constraints. If you ignore either direction, you end up with something brittle, confused, overbuilt, or perpetually unfinished.&lt;/p&gt;

&lt;p&gt;There is nothing sacred about any particular architectural style. Pick Domain-Driven, Clean, Vertical Slice, Hexagonal, Layered, or something entirely different. The choice matters far less than your consistency and your commitment to encapsulating concerns properly. Different problems require different arrangements of the same conceptual ingredients. From high altitude, two domains may look identical. Once you descend toward the details, you often discover that one is a bird and the other is an airplane. The trick is knowing when to zoom out and when to zoom in.&lt;/p&gt;

&lt;p&gt;Plenty of developers jump immediately into code, but the outside of the system is always the real beginning. What is it supposed to do. Who uses it. Who does it talk to. Who builds it. Who runs it. Who deploys it. Who monitors it. How do you prove it works. These questions define the problem space, and the problem space determines the boundaries and responsibilities your abstractions must reflect.&lt;/p&gt;

&lt;p&gt;Even something as small as a script must obey this reality.&lt;/p&gt;

&lt;p&gt;Consider a simple provisioning script. First it reads a certificate from the local filesystem so it can authenticate with a remote host. Next it opens an SFTP connection to a distribution server and retrieves a zip file. Then it extracts the archive to a temporary directory provided by the operating system. Finally it executes whatever installers or configuration commands the archive contains.&lt;/p&gt;

&lt;p&gt;On the surface this is straightforward, yet every step is shaped by the environment in which it operates. Tools differ between platforms. Available executables change. File paths and separators vary. Temporary directory locations vary. Even the existence or reliability of SFTP clients varies. None of this means we must implement every possible alternative upfront, but it does mean we should acknowledge the existence of alternatives and avoid designing ourselves into a corner where adding support later requires rewriting the entire script.&lt;/p&gt;

&lt;p&gt;This principle scales upward. You may choose to place your application data inside a database, but scattering SQL statements across your codebase is an anti-pattern in nearly any architecture not explicitly about database engines or ORM internals. Unless you are writing an RDBMS, data access is rarely the star of the show. The real substance lives in the application logic that interprets, transforms, regulates, or composes that data. Mixing data access concerns directly into that logic creates friction. Separating them reduces friction, which improves maintainability, which improves confidence, which improves speed.&lt;/p&gt;

&lt;p&gt;The guiding question is always the same: does this choice help my system model the problem more clearly, or does it merely model my current implementation?&lt;br&gt;
If it is the former, great. If it is the latter, you are accumulating technical debt even if the code looks clean.&lt;/p&gt;

&lt;p&gt;Abstractions aligned with the domain allow your system to grow gracefully. But abstractions aligned with your tooling force your system to grow awkwardly and inconsistently.&lt;/p&gt;

&lt;p&gt;This is the difference between designing from both directions and designing from just one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Behavior as the Backbone of Architecture
&lt;/h2&gt;

&lt;p&gt;At some point in every software project, the discussion inevitably turns to architecture. Engineers debate whether they should adopt Domain-Driven Design or Clean Architecture, whether their services ought to be hexagonal, layered, vertical-sliced, modular, or some other fashionable geometric configuration, and whether interfaces belong everywhere or nowhere at all. These conversations are interesting, even entertaining, but they often drift into abstraction for abstraction’s sake. The problem is rarely the patterns themselves; rather, it is that these debates frequently occur in a vacuum, disconnected from the actual behaviors the system must exhibit. Humans love patterns, but software only cares about whether it does the right thing.&lt;/p&gt;

&lt;p&gt;The most reliable way to design a system, therefore, is to begin with its behavior. A system exists to do something, and if we do not articulate that something clearly, everything downstream becomes guesswork and improvisation. This is precisely where behavior-driven development demonstrates its value. I explore this more deeply in &lt;a href="https://jerrettdavis.com/blog/posts/making-the-business-write-your-tests-with-bdd" rel="noopener noreferrer"&gt;BDD: Make the Business Write Your Tests&lt;/a&gt;, but in short, BDD forces us to express the responsibilities of the system in language that is precise, verifiable, and shared by both technical and nontechnical stakeholders. A behavior becomes a specification, a test, a boundary, and a contract all at once.&lt;/p&gt;

&lt;p&gt;From an architectural perspective, this shift in thinking is transformative. When we model the largest and most meaningful behaviors first and place an abstraction around them, we create an outer shell that defines the system at a conceptual level. From there, we move inward, breaking behaviors down iteratively into smaller and more specific responsibilities. Each division suggests a natural abstraction, but these abstractions are not arbitrary. They emerge directly from the behavior they represent. They are shaped not by the developer’s preferred patterns but by the needs of the domain itself. This recursive approach ensures that abstractions mirror intent rather than implementation details.&lt;/p&gt;

&lt;p&gt;Importantly, this recursion is not fractal. We are not attempting to subdivide reality endlessly. Rather, we refine behaviors only until they are sufficiently well understood to be implemented cleanly. Much as one does not explain quantum chromodynamics to teach someone how to scramble an egg, we do not decompose software beyond what clarity and accuracy require. And while many languages encourage the use of interfaces as the primary mechanism for abstraction, the interface is not the abstraction itself. It is merely a convenient way to enforce a contract. The real abstraction is the conceptual boundary it represents. Whether that boundary is expressed as an interface, a type, a configuration object, or a module is irrelevant as long as the contract is clear and consistent.&lt;/p&gt;

&lt;p&gt;This is why starting with abstractions like an &lt;code&gt;IHost&lt;/code&gt; that orchestrates an &lt;code&gt;IApplication&lt;/code&gt; works so well. These constructs mirror the system’s highest-level behaviors. Once defined, they allow us to drill inward, step by step, carving out responsibilities until the domain takes shape as a set of interlocking, behavior-aligned components. When abstractions are created this way, they tend to be stable. They align with the problem domain rather than the transient needs of a particular implementation, and therefore they seldom need to change unless the underlying behavior changes.&lt;/p&gt;

&lt;p&gt;Frequent modification of an abstraction is a warning sign. A well-formed abstraction typically changes only under three conditions: the business behavior has evolved, an overlooked edge case has surfaced, or the original abstraction contained a conceptual flaw. Outside of those circumstances, the need to repeatedly modify an abstraction usually indicates that its boundaries were drawn incorrectly. When adjusting one behavior forces changes across multiple components, the issue is rarely "too many" or "too few" abstractions in an abstract sense. Instead, it is a failure of alignment. The abstraction does not adequately contain the concerns it is supposed to model, and complexity is leaking out of its container and into the rest of the codebase.&lt;/p&gt;

&lt;p&gt;Modern tooling makes this problem even more evident. With the availability of source generators, analyzers, expressive type systems, code scaffolding, and dynamic configuration pipelines, there is increasingly little justification for sprawling boilerplate or repetitive structural code. Boilerplate is not a mark of engineering rigor. It is simply untested and uninteresting glue repeated dozens of times because someone did not take steps to automate it. Good abstractions, by contrast, elevate meaning. They allow the domain to be expressed directly without forcing the developer to wade through noise.&lt;/p&gt;

&lt;p&gt;This leads naturally to what I consider the ideal state of modern development: a system that is entirely automated from the moment code touches a repository until the moment it reaches a production-like environment. Compilation, testing, packaging, deployment, orchestration, and infrastructure provisioning should not require human involvement. The only manual step should be expressing intent in the form of new or updated behaviors. Every function that exists within the system should originate as a behavior-driven specification capable of running the entire application inside a controlled test environment, complete with containerized dependencies and UI automation tools such as Playwright. Those same tests should also be able to stub dependencies so the scenarios can run in isolation. When the system itself is treated as the first unit under test, orchestration becomes a priority rather than an afterthought.&lt;/p&gt;

&lt;p&gt;Achieving this level of automation depends on stability, and that stability depends on disciplined abstraction. Any element that may vary across environments, including configuration values, credentials, infrastructure, connection details, and policies, must be isolated behind settings and contracts that the application can consume without knowing anything about the environment it runs in. Once this encapsulation is in place, behavior-driven specifications can operate confidently, verifying the correctness of the system from the outside in even while its internal components remain free to evolve.&lt;/p&gt;

&lt;p&gt;Finally, it is worth stating explicitly that hand-writing repetitive boilerplate code in a CRUD-heavy application, such as repositories, controllers, mappers, DTOs, validators, or entire edge-to-edge layers, is not admirable craftsmanship. It is busywork. If you have twenty entities with identical structural behavior and you are manually writing twenty sets of nearly identical files, the issue is not insufficient discipline. It is insufficient automation. Whether through source generators, templates, reflection-based pipelines, or dynamic modules, these problems can and should be solved generically. Engineers should focus their manual effort on the places where meaning lives: the domain, the behavior, and the boundaries.&lt;/p&gt;

&lt;p&gt;Good abstractions do not eliminate complexity; they contain it. Bad abstractions distribute it. And behavior-driven, problem-first design is how we tell the difference.&lt;/p&gt;
&lt;h2&gt;
  
  
  From Story to Spec: Describing Behavior First
&lt;/h2&gt;

&lt;p&gt;To make this concrete, return to our original "Add Middle Initial to User Name Display" ticket. Most teams would handle this with a couple of unit tests directly against whatever &lt;code&gt;UserNameService&lt;/code&gt; or &lt;code&gt;UserNameFormatter&lt;/code&gt; happens to exist. The tests would exercise a particular class, call a particular method, and assert on a particular string. That can work, but it starts at the implementation, not at the behavior.&lt;/p&gt;

&lt;p&gt;If instead we begin with behavior, the specification sounds more like this:&lt;/p&gt;

&lt;p&gt;When a user has a middle name, show the middle initial between the first and last name.&lt;br&gt;
 When a user does not have a middle name, omit the gap entirely.&lt;br&gt;
 When a display style changes (for example, "formal" versus "informal"), the rules about how the middle initial appears should still hold.&lt;/p&gt;

&lt;p&gt;That is the contract. It does not mention classes, factories, or strategies. It talks about what the system must do from the outside.&lt;/p&gt;

&lt;p&gt;With something like my project &lt;a href="https://github.com/jerrettdavis/TinyBDD" rel="noopener noreferrer"&gt;TinyBDD&lt;/a&gt;, that kind of behavior becomes executable in a fairly direct way. Using the xUnit adapter, a scenario might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;TinyBDD.Xunit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Xunit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"User name display"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserNameDisplayScenarios&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TinyBddXunitBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Standard display includes middle initial when present"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;MiddleInitialIsRenderedWhenPresent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a user with first, middle, and last name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Ada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"M"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Lovelace"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"formatting the user name for standard display"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;UserNameDisplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Standard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"the result places the middle initial between first and last"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;formatted&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;formatted&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Ada M. Lovelace"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertPassed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Standard display omits missing middle initial"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;NoMiddleInitialWhenMissing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a user with only first and last name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Ada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Lovelace"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"formatting the user name for standard display"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;UserNameDisplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Standard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"no dangling spaces or periods appear"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;formatted&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;formatted&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Ada Lovelace"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertPassed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In these scenarios, the behavior is the first-class citizen. The test does not care whether you use a &lt;code&gt;UserNameDisplayStrategyFactory&lt;/code&gt;, a dependency-injected &lt;code&gt;IUserNameFormatter&lt;/code&gt;, or a static helper hidden in a dusty corner of your codebase. It cares that given a user, when you format their name, you get the right string.&lt;/p&gt;

&lt;p&gt;The abstractions are already visible in the code, but only as a side effect of expressing behavior:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UserName&lt;/code&gt; represents the domain concept of a person’s name, not a UI or persistence model.&lt;br&gt;
 &lt;code&gt;UserNameDisplay.Standard&lt;/code&gt; represents a particular display style that the business cares about.&lt;br&gt;
 The behavior is encoded in the transition from &lt;code&gt;UserName&lt;/code&gt; to the formatted string, not in a particular class hierarchy.&lt;/p&gt;

&lt;p&gt;Notice what is not present: we do not have separate strategies for every permutation of name structure, locale, and display preference. We have a single coherent abstraction around "displaying a user name in the standard way," and the test drives the rules we actually need.&lt;/p&gt;
&lt;h2&gt;
  
  
  Letting Abstractions Fall Out of the Domain
&lt;/h2&gt;

&lt;p&gt;Once you have a behavior-focused spec, the abstractions almost draw themselves. One reasonable implementation might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;UserName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;First&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Middle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Last&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IUserNameDisplay&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StandardUserNameDisplay&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IUserNameDisplay&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Middle&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;First&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Middle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Last&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;First&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Last&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserNameDisplay&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IUserNameDisplay&lt;/span&gt; &lt;span class="n"&gt;Standard&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StandardUserNameDisplay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not an argument that every trivial formatting problem deserves an interface and a concrete class. You could inline this logic in a static helper and your tests above would still pass. The point is that the abstraction here is small, meaningful, and directly aligned with the behavior we care about. If later the domain grows to include multiple display styles, cultures, or localization concerns, there is already a clear seam to extend. You can introduce additional &lt;code&gt;IUserNameDisplay&lt;/code&gt; implementations where and when they are genuinely needed, not because a pattern catalog declared that every noun deserves a factory.&lt;/p&gt;

&lt;p&gt;If, however, you discover that adding a new behavior requires touching half the classes in the system, that is a sign you have modeled implementation variants rather than domain concepts. The behavior spec remains constant; the code churn reveals where your abstractions are misaligned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling the Same Idea Up to the System Level
&lt;/h2&gt;

&lt;p&gt;So far this is all very local. A name goes in, a formatted string comes out. Real systems have much more interesting behaviors: accepting traffic, orchestrating workflows, integrating with external services, healing from transient failures, deploying safely, and so on.&lt;/p&gt;

&lt;p&gt;The same discipline still applies. You can treat the application itself as the unit under test and express its behavior with the same style of specification. A high-level scenario might read something like this:&lt;/p&gt;

&lt;p&gt;Given a configured application host and its dependencies&lt;br&gt;
 When the host starts&lt;br&gt;
 Then the public API responds to a health probe&lt;br&gt;
 And all critical services report healthy&lt;br&gt;
 And any failing dependency is surfaced clearly rather than silently ignored&lt;/p&gt;

&lt;p&gt;As an executable TinyBDD scenario, that might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;TinyBDD.Xunit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Xunit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Application startup and health"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationHealthScenarios&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TinyBddXunitBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Host starts and exposes a healthy API surface"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;HostStartsAndReportsHealthy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a test host with default configuration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;TestApplicationHost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDefault&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"the host is started"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"the health endpoint returns OK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;AssertHealthEndpointOk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"all critical health checks pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;AssertCriticalChecksPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AssertPassed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AssertHealthEndpointOk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestApplicationHost&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// This could exercise a real HTTP endpoint against a TestServer or containerized instance.&lt;/span&gt;
        &lt;span class="c1"&gt;// The assertion lives here, but the behavior is defined in the scenario above.&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NotImplementedException&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AssertCriticalChecksPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestApplicationHost&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Could query IHealthCheckPublisher, metrics, logs, or an in-memory probe endpoint.&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NotImplementedException&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation details behind &lt;code&gt;TestApplicationHost&lt;/code&gt; are intentionally omitted here, because they are not the main point. What matters is that at the boundary, we are still describing behavior: the host starts, the API responds, health checks pass. Internally, &lt;code&gt;TestApplicationHost&lt;/code&gt; can wrap an &lt;code&gt;IHost&lt;/code&gt;, use Testcontainers, spin up a &lt;code&gt;WebApplicationFactory&lt;/code&gt;, or compose a full stack in Docker. The abstraction exists to let the behavior remain stable while infrastructure details evolve.&lt;/p&gt;

&lt;p&gt;This is the same pattern you used on the small scale with &lt;code&gt;UserNameDisplay&lt;/code&gt;, only now it operates at the level of the entire application. The outermost abstraction represents the system as it is experienced from the outside. Everything underneath exists to satisfy that experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Declarative Core, Automated Edge
&lt;/h2&gt;

&lt;p&gt;Once specifications like these exist, they become the backbone of your automation. The natural end state is a development flow where:&lt;/p&gt;

&lt;p&gt;A new behavior is introduced as a TinyBDD scenario.&lt;br&gt;
 That scenario boots the application in a realistic but controlled environment.&lt;br&gt;
 The rest of the stack compiles, configures, and deploys itself into a test harness without manual intervention.&lt;br&gt;
 The same scenarios run in local, CI, and pre-production environments, with only configuration differing between them.&lt;/p&gt;

&lt;p&gt;The actual application code can then remain highly declarative. Controllers or handlers describe what should happen when a request arrives. Domain services express rules and policies in terms of value objects and aggregates. Infrastructure concerns hide behind interfaces and adapters. Source generators or templates can remove boilerplate around repetitive CRUD or mapping concerns. The tests remain focused on behavior: does the system do what we said it would do.&lt;/p&gt;

&lt;p&gt;Abstractions in this world are not ornamental. They are the scaffolding that holds the behavior in place while the infrastructure and implementation details shift around it. As long as the specifications stay clear and the boundaries remain aligned with the domain, you can move quickly without losing correctness. And if you ever find yourself adding yet another &lt;code&gt;UserNameDisplayStrategyFactoryFactory&lt;/code&gt; just to keep a scenario passing, you will at least have a clear, behavior-centric lens through which to recognize that something has gone wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Patterns, Declarative Thinking, and Solving the General Case
&lt;/h2&gt;

&lt;p&gt;Before closing, there is one more point worth addressing, because it tends to resurface in every conversation about abstraction: the role of design patterns. Patterns are often misunderstood in practice. For some teams they become a dictionary of shapes to copy. For others they become a superstition, something to be avoided because they "feel enterprise." In reality, design patterns are nothing more than reusable expressions of relationships that occur frequently in software. They only become harmful when applied without context.&lt;/p&gt;

&lt;p&gt;Used well, patterns are a form of declarative modeling. They describe how things relate, not how many classes must be introduced to satisfy a template. This distinction is one reason I created &lt;a href="https://github.com/jerrettdavis/patternkit" rel="noopener noreferrer"&gt;PatternKit&lt;/a&gt;&lt;br&gt;
which contains fluent implementations of every GoF pattern. The aim is not to celebrate patterns for their own sake, but to show that they can be expressed clearly and idiomatically, without the ceremony that has accumulated around them over the past few decades. A fluent strategy or builder is readable because it conveys meaning, not because it adheres to a UML diagram. A properly shaped composite or decorator is useful because it matches the problem at hand, not because the catalog says "now is the time."&lt;/p&gt;

&lt;p&gt;Patterns at their best are accelerants for thought. They give structure to complex behavior. They reveal seams in the domain. They help us express intent without prescribing a particular class arrangement. When applied declaratively, patterns become lightweight tools that reinforce clarity rather than obstacles that obscure it.&lt;/p&gt;

&lt;p&gt;This is the same principle that guides good abstractions. We should always aim to solve the general case when appropriate, rather than re-solving the same narrow problem in twenty different places. Shared operations belong in helpers or extensions not because we want fewer lines of code, but because meaning belongs in one place rather than scattered across many. Wrapping behavior in a well-designed abstraction is not indulgence; it is about shaping the domain so the rest of the system can grow without friction. Once the domain is sufficiently modeled, higher-order helpers, pipelines, or policy objects can provide a unified vocabulary for orchestrating that domain. These are the moments when patterns shine: when they articulate a common structure behind several similar problems and offer a clean way to express the variation between them.&lt;/p&gt;

&lt;p&gt;Patterns should not be forced; they should emerge. If you find yourself retrofitting the domain to suit a pattern, the pattern is wrong. If a pattern clarifies the domain, it is the right one. When in doubt, your behaviors and your domain model will tell you the truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;At every scale, from a one-line helper to a distributed system with dozens of services, the same principles hold. Begin with behavior. Shape abstractions around the real problems rather than your implementation preferences. Allow patterns to emerge naturally when they clarify meaning. Lean on automation and declarative structures to eliminate noise. Let stability arise from good boundaries rather than rigid frameworks. And above all, keep re-examining the domain as it evolves. Systems live for years; code is rewritten many times. The only reliable compass is the domain itself.&lt;/p&gt;

&lt;p&gt;You do not hate abstractions. What you hate are the wrong ones: misplaced layers, premature hierarchies, needless ceremony, half-understood patterns applied because someone did not want to think. But abstractions that arise from behavior and domain, shaped carefully and used intentionally, are not burdens. They are the tools that let us build systems that last.&lt;/p&gt;

&lt;p&gt;And if you stay anchored to this style of approach, you won’t end up navigating a small cavern system of strategies and factories and wondering how a middle initial turned into a guided tour of every pattern someone ever took a little too seriously. You won’t need to fear abstraction at all. Instead, you will wield it where it belongs.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>programming</category>
      <category>design</category>
      <category>development</category>
    </item>
    <item>
      <title>BDD: Make the Business Write Your Tests</title>
      <dc:creator>Jerrett Davis</dc:creator>
      <pubDate>Mon, 01 Sep 2025 22:48:53 +0000</pubDate>
      <link>https://dev.to/jerrettdavis/bdd-make-the-business-write-your-tests-27c9</link>
      <guid>https://dev.to/jerrettdavis/bdd-make-the-business-write-your-tests-27c9</guid>
      <description>&lt;p&gt;What if the business wrote the tests, and developers just made them pass?  &lt;/p&gt;

&lt;p&gt;That's the promise of behavior-driven development (BDD). Instead of developers guessing at requirements, chasing &lt;br&gt;
Slack threads, and interpreting vague Jira tickets, we let the people who know the &lt;em&gt;why&lt;/em&gt; express it in a form &lt;br&gt;
that's precise enough to execute.  &lt;/p&gt;

&lt;p&gt;Throughout my career, I've worked with teams of all shapes and sizes, and one pattern is universal:&lt;br&gt;&lt;br&gt;
nobody &lt;em&gt;loves&lt;/em&gt; writing tests. Most developers grudgingly agree they're important, but tests are often seen &lt;br&gt;
as a tax—time spent writing code that doesn't "ship features."  &lt;/p&gt;

&lt;p&gt;The result is predictable: coverage gaps, brittle suites, and requirements that live in Confluence but &lt;br&gt;
never make it into code. Testing becomes a burden instead of a superpower.&lt;/p&gt;

&lt;p&gt;BDD flips that dynamic. Instead of treating tests as a chore, it turns them into a shared language between &lt;br&gt;
developers, QA, and the business. Suddenly, everyone is speaking the same language about features and &lt;br&gt;
scenarios, and tests become the living documentation of what the system is supposed to do.  &lt;/p&gt;

&lt;p&gt;Before we dive into how this works, let's establish a few "Laws of Testing." These aren't divine truths, &lt;br&gt;
but they're a solid starting point—guidelines that help ensure tests actually drive design instead of &lt;br&gt;
just describing what already exists.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Laws of Testing
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;No application code will be written until tests are defined.
&lt;/li&gt;
&lt;li&gt;Systems should be tested as they are meant to be used.
&lt;/li&gt;
&lt;li&gt;Automate tests to the maximum extent technically and financially feasible.
&lt;/li&gt;
&lt;li&gt;Every business requirement must have a corresponding test. If the requirement changes, the test must change.
&lt;/li&gt;
&lt;li&gt;Any code not covered by realistic, automated tests should be treated as magic—and magic should not be trusted.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice that I'm not prescribing a specific tool, framework, or level of granularity. These laws simply give you &lt;br&gt;
a pragmatic foundation to make testing a first-class part of building software, not an afterthought.  &lt;/p&gt;
&lt;h2&gt;
  
  
  Behavior-Driven Development
&lt;/h2&gt;

&lt;p&gt;Now for the fun part: putting this into practice.  &lt;/p&gt;

&lt;p&gt;Behavior-Driven Development (&lt;em&gt;BDD&lt;/em&gt;) is designed to bridge the gap between business stakeholders, users, developers, &lt;br&gt;
QA, and everyone else involved in a project. There are many flavors of BDD, but at their core, they all focus on telling &lt;br&gt;
cohesive, testable stories using a shared Domain-Specific Language (DSL).  &lt;/p&gt;

&lt;p&gt;There's a long-standing joke in IT—still true even in the post-ChatGPT era—that it's easier to teach a subject-matter &lt;br&gt;
expert to code than it is to get them to clearly articulate what they actually want built.  &lt;/p&gt;

&lt;p&gt;Over the years, countless tools have promised to help non-technical folks bring their ideas to life, but theory and &lt;br&gt;
practice rarely align. &lt;em&gt;Everything works in theory.&lt;/em&gt; We can keep building better tools to bridge the gap between &lt;br&gt;
decision-makers and developers, but no tool can prevent the inevitable: &lt;strong&gt;requirements change&lt;/strong&gt;.  &lt;/p&gt;

&lt;p&gt;Business needs evolve constantly. Even in a perfectly stable market, a company's technology stack would still have to &lt;br&gt;
adapt—whether to security updates, API deprecations, or new compliance requirements. That's why business and technology &lt;br&gt;
teams need a living contract: a shared, human-readable specification of what's supposed to exist, expressed in language &lt;br&gt;
that everyone can understand.  &lt;/p&gt;

&lt;p&gt;BDD provides exactly that. It takes the "what" from the business and turns it into executable specifications for developers, &lt;br&gt;
ensuring the system stays aligned with today's needs—not just the assumptions written down last quarter.&lt;/p&gt;
&lt;h2&gt;
  
  
  What BDD Actually Is
&lt;/h2&gt;

&lt;p&gt;Behavior-Driven Development (BDD) is essentially turning your acceptance criteria into executable code.  &lt;/p&gt;

&lt;p&gt;If your company already writes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Acceptance Criteria in Jira or Azure DevOps&lt;/li&gt;
&lt;li&gt;Business Requirement Documents (BRDs) in Word or Confluence&lt;/li&gt;
&lt;li&gt;Test Scripts for QA teams to follow manually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…you're already halfway there. BDD simply takes those artifacts, expresses them in a precise, human-readable format, &lt;br&gt;
and wires them into your test suite so they can be run automatically.  &lt;/p&gt;

&lt;p&gt;Instead of existing only as text that humans must interpret, your requirements become living specifications that &lt;br&gt;
are always in sync with what the system actually does. If the system drifts, the tests fail — letting you catch gaps &lt;br&gt;
before production users do.&lt;/p&gt;

&lt;p&gt;Key ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ubiquitous language&lt;/strong&gt;: agree on domain terms and reuse them in requirements, tests, and code.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outside-in&lt;/strong&gt;: start from observable outcomes (what users and the business care about) and let those drive your implementation.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Executable documentation&lt;/strong&gt;: your feature files become a source of truth that stays up to date because failing tests 
force the team to update them when the business changes its mind.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Why It Clicks with the Business
&lt;/h2&gt;

&lt;p&gt;BDD speaks the same language the business already uses.  &lt;/p&gt;

&lt;p&gt;Instead of:  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Verify that gold members receive free shipping on orders over $10." &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Buried in a PDF or Confluence page, we can write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; Gold member gets free shipping
  &lt;span class="nf"&gt;Given &lt;/span&gt;the customer is a &lt;span class="s"&gt;"gold"&lt;/span&gt; member
  &lt;span class="nf"&gt;And &lt;/span&gt;they have a cart totaling $12.00
  &lt;span class="nf"&gt;When &lt;/span&gt;they checkout with standard shipping
  &lt;span class="nf"&gt;Then &lt;/span&gt;the shipping cost is $0.00
  &lt;span class="nf"&gt;And &lt;/span&gt;the order total is $12.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stakeholders can read and confirm this just like they'd review an acceptance test — but now this doubles as a&lt;br&gt;
machine-executable test that can be run in CI.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to Start Without Overhauling Everything
&lt;/h2&gt;

&lt;p&gt;You don't need to replace all your documentation overnight. Try this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take an existing acceptance criterion for a small feature.&lt;/li&gt;
&lt;li&gt;Rewrite it as a Gherkin scenario.&lt;/li&gt;
&lt;li&gt;Wire up just enough step definitions to make it run.&lt;/li&gt;
&lt;li&gt;Let it fail (red).&lt;/li&gt;
&lt;li&gt;Write or adjust code until it passes (green).&lt;/li&gt;
&lt;li&gt;Refactor code &lt;em&gt;and&lt;/em&gt; scenario until both are clear and maintainable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it — you've just done outside-in development. Over time, you can replace more of your static BRDs and&lt;br&gt;
test scripts with living specs that run automatically and stay current.&lt;/p&gt;



&lt;p&gt;By framing BDD as an evolution of what teams already do, it becomes less intimidating: you're not adding "one more thing,"&lt;br&gt;
you're making what you already write executable, consistent, and always up to date.&lt;/p&gt;
&lt;h2&gt;
  
  
  Starting from Existing Code (The Developer + QA Perspective)
&lt;/h2&gt;

&lt;p&gt;Of course, not every team has pristine requirements or well-maintained acceptance criteria.&lt;br&gt;&lt;br&gt;
Sometimes the &lt;em&gt;only&lt;/em&gt; source of truth is the code itself, or a set of outdated manual test scripts in a shared folder.&lt;br&gt;&lt;br&gt;
That's okay — you can still apply BDD principles to what you already have.  &lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Surface What the Code Already Does
&lt;/h3&gt;

&lt;p&gt;Start by exploring the system from the outside in, and not by reading the code first.  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Walk through the application like a user would: click buttons, submit forms, run API calls.
&lt;/li&gt;
&lt;li&gt;Write down what you observe in plain language, almost as if you were writing a support guide.
&lt;/li&gt;
&lt;li&gt;Identify key flows: logins, purchases, data exports, admin tasks, error handling.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're essentially reverse-engineering the behavior that already exists. The goal isn't to perfectly model the internals, but to describe what happens when X occurs.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Capture Behavior as Scenarios
&lt;/h3&gt;

&lt;p&gt;Turn those observations into scenarios, even if they're manual at first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gherkin"&gt;&lt;code&gt;&lt;span class="kn"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; User can reset password
  &lt;span class="nf"&gt;Given &lt;/span&gt;I am on the login page
  &lt;span class="nf"&gt;When &lt;/span&gt;I click &lt;span class="s"&gt;"Forgot password"&lt;/span&gt;
  &lt;span class="nf"&gt;And &lt;/span&gt;I submit my email address
  &lt;span class="nf"&gt;Then &lt;/span&gt;I receive a password reset link by email
  &lt;span class="nf"&gt;And &lt;/span&gt;I can set a new password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;This becomes your backfill documentation and a checklist you can use to validate future changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Choose a Testing Library
&lt;/h3&gt;

&lt;p&gt;Once you have a handful of scenarios, pick a suitable tool that matches your tech stack.&lt;br&gt;
It doesn't have to be fancy — choose something that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parse Gherkin or similar DSL (&lt;a href="https://cucumber.io/docs/installation/" rel="noopener noreferrer"&gt;Cucumber&lt;/a&gt;, SpecFlow/&lt;a href="https://reqnroll.net/" rel="noopener noreferrer"&gt;Reqnroll&lt;/a&gt;, &lt;a href="https://behave.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;Behave&lt;/a&gt;, &lt;a href="https://github.com/JerrettDavis/TinyBDD" rel="noopener noreferrer"&gt;TinyBDD&lt;/a&gt; 😉, etc.)&lt;/li&gt;
&lt;li&gt;Run in your CI/CD pipeline&lt;/li&gt;
&lt;li&gt;Integrate with the type of app you have (UI automation, API tests, service-level tests)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't over-engineer at this stage. The goal is simply to make a scenario run end-to-end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Build Thin Step Definitions
&lt;/h3&gt;

&lt;p&gt;When you wire up steps to code, keep the step definitions thin and reusable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat them like glue code — they orchestrate, not implement.&lt;/li&gt;
&lt;li&gt;Push logic into abstractions (page objects, API clients, domain helpers).&lt;/li&gt;
&lt;li&gt;Keep language aligned with the business terms you captured earlier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation makes your steps easy to read and your automation maintainable even as your app changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Automate the Most Valuable Flows First
&lt;/h3&gt;

&lt;p&gt;Don't try to automate everything on day one. Pick a few high-value, low-volatility flows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Happy-path checkouts&lt;/li&gt;
&lt;li&gt;Core login and authentication&lt;/li&gt;
&lt;li&gt;Critical reporting or data pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start small, get them running reliably in CI, and expand gradually. The point is not to hit 100% coverage overnight, but to start to &lt;/p&gt;

&lt;h2&gt;
  
  
  gain comfort and momentum in writing BDD tests end-to-end.
&lt;/h2&gt;

&lt;p&gt;By working backward from the app's behavior, you create a bridge between what the code does and what the business expects &lt;br&gt;
even when no documentation exists. Over time, your automated scenarios become the new source of truth, letting developers and QA refactor or ship new features with confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing It All Together
&lt;/h2&gt;

&lt;p&gt;Whether you're starting from crisp acceptance criteria or working backwards from a codebase that only lives in developers' heads, &lt;br&gt;
the goal of BDD is the same: &lt;strong&gt;close the gap between what the business needs and what the code does&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You don't have to overhaul your entire testing strategy in a single sprint. You just have to start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write one scenario.&lt;/li&gt;
&lt;li&gt;Get it running end-to-end.&lt;/li&gt;
&lt;li&gt;Share it with your team.&lt;/li&gt;
&lt;li&gt;Keep going.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over time, you'll build up a living specification that grows with the system, catching regressions early and making onboarding new developers dramatically easier.  &lt;/p&gt;

&lt;h3&gt;
  
  
  What a Mature BDD Practice Looks Like
&lt;/h3&gt;

&lt;p&gt;A well-adopted BDD process creates a feedback loop that keeps everyone aligned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Business &amp;amp; product teams&lt;/strong&gt; write or review scenarios as part of refinement.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developers&lt;/strong&gt; implement features by making those scenarios pass.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA&lt;/strong&gt; contributes new scenarios for edge cases and validates existing ones stay green.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD pipelines&lt;/strong&gt; run the whole suite automatically, so everyone knows the current state of the system at a glance.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When this loop is healthy, you get a shared understanding of what "done" really means, and confidence that your software still works tomorrow, next quarter, and next year.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;BDD isn't a silver bullet, but it &lt;em&gt;is&lt;/em&gt; a forcing function for clearer requirements, more reliable software, and tighter collaboration across teams.  &lt;/p&gt;

&lt;p&gt;If you've ever wished the business could "just write the tests," BDD is the closest thing we have to that dream. Start small, &lt;br&gt;
stay consistent, and watch as those scenarios turn into a living, breathing specification that guides your development for years to come.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>testing</category>
      <category>coding</category>
      <category>devops</category>
    </item>
    <item>
      <title>What is "Code"?</title>
      <dc:creator>Jerrett Davis</dc:creator>
      <pubDate>Mon, 01 Sep 2025 22:35:00 +0000</pubDate>
      <link>https://dev.to/jerrettdavis/what-is-code-1o4o</link>
      <guid>https://dev.to/jerrettdavis/what-is-code-1o4o</guid>
      <description>&lt;p&gt;Technology is a wonderful thing. Humans, and perhaps even our direct ancestors, have been employing it for millennia. From stone tools to quantum computers, humans can't seem to resist tinkering with whatever the universe gives them.&lt;/p&gt;

&lt;p&gt;And with even our most remote, uncontacted relatives still in possession of weapons and shelter, it's safe to say every person on this planet experiences technology from birth to their final days. Right now, as you read this, you're surrounded by, and literally in contact with, an almost uncountable web of human inventions.&lt;/p&gt;

&lt;p&gt;Technology shapes our lives, and in turn, we shape its future. It can be used for feats of great creation or acts of horrific destruction. Among all its branches, one of the most powerful and most accessible is &lt;strong&gt;code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You don't need a factory, a lab, or a workshop to work with code. All it takes is a computer and a spark of curiosity. If you've ever stacked LEGO bricks into something from your imagination, built a spreadsheet formula to save hours of work, set a coffee maker to brew before you wake, or fixed something in a pinch with whatever was at hand, you've tapped into the same problem-solving instinct that drives programming.&lt;/p&gt;

&lt;p&gt;At its heart, coding is the act of taking what you have and arranging it into something that does what you want. It's imagination with rules, creativity with feedback. And once you've seen something you've built come alive, whether it's a blinking LED, a tool that saves someone's day, or an app that makes a stranger smile, it's hard not to want to build more.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is "Code"?
&lt;/h2&gt;

&lt;p&gt;Code comes in many flavors. There are programming languages like C#, Assembly, C++, Rust, Python, and countless others, each with its own syntax, style, and purpose. There's &lt;strong&gt;G-code&lt;/strong&gt;, the set of instructions understood by CNC mills, 3D printers, lasers, and other computer-controlled machines. Even music notation is a kind of code, a written sequence of symbols that, when interpreted, produces something meaningful.&lt;/p&gt;

&lt;p&gt;The “code” most familiar to all of us is &lt;strong&gt;human language&lt;/strong&gt;. Spoken or written, it can form relationships, transfer knowledge, and inspire change. But language is inherently ambiguous, which makes it ill-suited for telling computers exactly what to do. That's why programming languages exist: precise dialects designed for humans to read and for machines to execute without hesitation or confusion.&lt;/p&gt;

&lt;p&gt;Put simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Code is a system of signals, symbols, letters, words, or other constructs used to convey a message.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At the very bottom of every modern computer lies &lt;strong&gt;binary&lt;/strong&gt; — two values, usually 1 and 0, representing “on” and “off” or “true” and “false.” Working directly in binary is tedious, so we created layers of abstraction to make our lives easier.&lt;/p&gt;

&lt;p&gt;A step above binary is &lt;strong&gt;assembly language&lt;/strong&gt;. Assembly comes in many dialects, each tied to the instruction set of a specific processor. It's still “close to the metal” but far more readable than raw ones and zeros.&lt;/p&gt;




&lt;h3&gt;
  
  
  A (Somewhat Accurate and Still Overly Dramatic) History of Programming Languages
&lt;/h3&gt;

&lt;p&gt;The history of programming languages is a tapestry of invention, frustration, and the occasional all-nighter. Imagine it told as a fantasy epic.&lt;/p&gt;

&lt;p&gt;First came &lt;strong&gt;Binary&lt;/strong&gt;, and from Binary were forged all other tongues of the machine. But the work was slow, and the scribes of silicon longed for an easier way to command their creations.&lt;/p&gt;

&lt;p&gt;In the early 1950s, pioneers began to shape new languages: &lt;strong&gt;Regional Assembly Language&lt;/strong&gt; (1951), &lt;strong&gt;Autocode&lt;/strong&gt; (1952), and &lt;strong&gt;IPL&lt;/strong&gt; (1954), the forerunner to LISP. Grace Hopper's &lt;strong&gt;FLOW-MATIC&lt;/strong&gt; (1955) would pave the way to &lt;strong&gt;COBOL&lt;/strong&gt; (1959), while &lt;strong&gt;FORTRAN&lt;/strong&gt; (1957) brought the first compiler to life. The 1960s saw the birth of &lt;strong&gt;LISP&lt;/strong&gt; (1958), &lt;strong&gt;ALGOL&lt;/strong&gt; (1958, 1960), &lt;strong&gt;BASIC&lt;/strong&gt; (1964), &lt;strong&gt;PL/I&lt;/strong&gt; (1964), &lt;strong&gt;BCPL&lt;/strong&gt; (1967), and &lt;strong&gt;B&lt;/strong&gt; (1969), leading to the mighty &lt;strong&gt;C&lt;/strong&gt; (1972).&lt;/p&gt;

&lt;p&gt;From the 1970s onward, the floodgates opened: &lt;strong&gt;Pascal&lt;/strong&gt; (1970), &lt;strong&gt;Smalltalk&lt;/strong&gt; (1972), &lt;strong&gt;Prolog&lt;/strong&gt; (1972), &lt;strong&gt;SQL&lt;/strong&gt; (1978), &lt;strong&gt;C++&lt;/strong&gt; (1980), &lt;strong&gt;Ada&lt;/strong&gt; (1983), &lt;strong&gt;Perl&lt;/strong&gt; (1987), &lt;strong&gt;Python&lt;/strong&gt; (1991), &lt;strong&gt;Java&lt;/strong&gt; (1995), &lt;strong&gt;JavaScript&lt;/strong&gt; (1995), &lt;strong&gt;Ruby&lt;/strong&gt; (1995), &lt;strong&gt;PHP&lt;/strong&gt; (1995), and many more.&lt;/p&gt;

&lt;p&gt;Each era brought its own heroes, philosophies, and quirks. Some languages were swift and elegant, others sprawling and stubborn. But all of them, in their way, expanded what was possible.&lt;/p&gt;

&lt;p&gt;Today, we live in an age of abundance. We have languages, frameworks, and tools for nearly every conceivable task. And if the exact tool you need doesn't exist, the beauty of code is that you can create it yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Code Matters
&lt;/h2&gt;

&lt;p&gt;Code is power, not in the dystopian sense, but in the sense that it lets one person shape behavior, automate work, and solve problems in ways that scale far beyond their own hands.&lt;/p&gt;

&lt;p&gt;With code, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automate boring tasks&lt;/li&gt;
&lt;li&gt;Create tools that help people&lt;/li&gt;
&lt;li&gt;Control machines in the physical world&lt;/li&gt;
&lt;li&gt;Build art, games, and interactive experiences&lt;/li&gt;
&lt;li&gt;Invent entirely new kinds of technology&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need to be a professional programmer to benefit from learning it. Even small bits of code can make life easier, save time, or open the door to entirely new possibilities.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;This post answered “What is code?” in broad strokes: its forms, history, and why it matters. In the next part of this series, we'll start laying the &lt;strong&gt;foundations of programming&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Variables &amp;amp; Data Types&lt;/strong&gt; — the labeled containers of programming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operators&lt;/strong&gt; — the tools for working with your data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic &amp;amp; Flow&lt;/strong&gt; — the recipes that decide what happens when.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By the time we're done, you'll not just &lt;em&gt;know&lt;/em&gt; what code is, you'll be able to write it, read it, and use it to build something that matters to you.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>coding</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Automating Nightly Local Database Refreshes from Azure Blob Storage with Docker</title>
      <dc:creator>Jerrett Davis</dc:creator>
      <pubDate>Thu, 29 Feb 2024 16:54:28 +0000</pubDate>
      <link>https://dev.to/jerrettdavis/automating-nightly-local-database-refreshes-from-azure-blob-storage-with-docker-3api</link>
      <guid>https://dev.to/jerrettdavis/automating-nightly-local-database-refreshes-from-azure-blob-storage-with-docker-3api</guid>
      <description>&lt;ul&gt;
&lt;li&gt;Background&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;
Setting up the Docker Container

&lt;ul&gt;
&lt;li&gt;Base Dockerfile&lt;/li&gt;
&lt;li&gt;Install Azure CLI&lt;/li&gt;
&lt;li&gt;Install SqlPackage&lt;/li&gt;
&lt;li&gt;Entrypoint&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

The Scripts

&lt;ul&gt;
&lt;li&gt;entrypoint.sh&lt;/li&gt;
&lt;li&gt;initialize-database-and-jobs.sh&lt;/li&gt;
&lt;li&gt;download-latest.sh&lt;/li&gt;
&lt;li&gt;enable-authentication.sql&lt;/li&gt;
&lt;li&gt;reimport-database.sh&lt;/li&gt;
&lt;li&gt;kill-all-connections.sql&lt;/li&gt;
&lt;li&gt;Final Directory Structure and Dockerfile Changes&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Building the Docker Container&lt;/li&gt;

&lt;li&gt;Testing the Container&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;h1&gt;
  
  
  Background
&lt;/h1&gt;

&lt;p&gt;In cloud-hosted applications, it is common to restrict access to production databases. This is a good practice, but it can&lt;br&gt;
make it difficult for various teams to access the data they need for development, testing, reporting, and data analysis.&lt;br&gt;
One way to solve this problem is to create a daily process that copies the production database to a location that is accessible&lt;br&gt;
to the teams that need it. In this article, we will create a docker container that will download the latest backup of a&lt;br&gt;
production database from Azure Blob Storage and restore it to a local SQL Server instance.&lt;/p&gt;
&lt;h1&gt;
  
  
  Prerequisites
&lt;/h1&gt;

&lt;p&gt;This article assumes that you have already set up an automation to back up your production database to Azure Blob Storage.&lt;br&gt;
If you have not done this, you can follow the instructions in this article: &lt;a href="https://techcommunity.microsoft.com/t5/azure-database-support-blog/automate-exporting-of-azure-sql-database-as-bacpac-to-blog/ba-p/2409213" rel="noopener noreferrer"&gt;Automating Nightly Database Backups to Azure Blob Storage&lt;/a&gt;.&lt;br&gt;
The script in the article is reaching end of life, and an updated version can be found &lt;a href="https://github.com/josegomera/AzureAutomation/tree/master/scripts/sqlDatabase" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Docker must be installed and configured to used Linux containers on your local machine. If you do not&lt;br&gt;
have Docker installed, you can download it from &lt;a href="https://www.docker.com/products/docker-desktop" rel="noopener noreferrer"&gt;Docker's website&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;
  
  
  Setting up the Docker Container
&lt;/h1&gt;

&lt;p&gt;The first step is to create a Dockerfile that will be used to build the container. The Dockerfile will contain the instructions&lt;br&gt;
on how to configure the container and what commands to run when the container is started. For our environment, we will&lt;br&gt;
require Microsoft SQL Server to be installed in the container. We will use the official Microsoft SQL Server Docker image as a base.&lt;/p&gt;
&lt;h2&gt;
  
  
  Base Dockerfile
&lt;/h2&gt;

&lt;p&gt;Create a new directory on your local machine and create a file called &lt;code&gt;Dockerfile&lt;/code&gt; in the directory. Add the following content to the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; mcr.microsoft.com/mssql/server:2022-CU11-ubuntu-22.04&lt;/span&gt;

&lt;span class="c"&gt;# Set environment variables for the container&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; ACCOUNT_NAME&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; ACCOUNT_NAME=$ACCOUNT_NAME&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; ACCOUNT_KEY&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; ACCOUNT_KEY=$ACCOUNT_KEY&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; CONTAINER_NAME&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; CONTAINER_NAME=$CONTAINER_NAME&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; CRON_SCHEDULE="0 4 * * *"&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; CRON_SCHEDULE=$CRON_SCHEDULE&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; DATABASE_NAME=MyDatabase&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; DATABASE_NAME=$DATABASE_NAME&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; MSSQL_SA_PASSWORD=yourStrong(!)Password&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; ACCEPT_EULA=Y&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; MSSQL_PID=Developer&lt;/span&gt;

&lt;span class="c"&gt;# Create a working directory for our tools and scripts and copy all the files from the host machine to the container&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /sql&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /sql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install Azure CLI
&lt;/h2&gt;

&lt;p&gt;To find and download the latest backup of the production database, we will need to install the Azure CLI in the container.&lt;br&gt;
We'll also need wget, cron, unzip, and a few other utilities to help us automate the process. Microsoft does offer a script&lt;br&gt;
to install the Azure CLI, but in my testing it did not work as expected in the Docker container. Instead, we will use&lt;br&gt;
the &lt;a href="https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-linux?pivots=apt#option-2-step-by-step-installation-instructions" rel="noopener noreferrer"&gt;manual installation instructions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Update the &lt;code&gt;Dockerfile&lt;/code&gt; to include the following lines after the &lt;code&gt;WORKDIR&lt;/code&gt; line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Must be root to install packages&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; root&lt;/span&gt;

&lt;span class="c"&gt;# Install Dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    unzip cron wget apt-transport-https &lt;span class="se"&gt;\
&lt;/span&gt;    software-properties-common ca-certificates curl &lt;span class="se"&gt;\
&lt;/span&gt;    apt-transport-https lsb-release gnupg &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# Install az cli&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/apt/keyrings &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    curl &lt;span class="nt"&gt;-sLS&lt;/span&gt; https://packages.microsoft.com/keys/microsoft.asc | &lt;span class="se"&gt;\
&lt;/span&gt;        gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; | &lt;span class="se"&gt;\
&lt;/span&gt;        &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/keyrings/microsoft.gpg &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chmod &lt;/span&gt;go+r /etc/apt/keyrings/microsoft.gpg &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nv"&gt;AZ_DIST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lsb_release &lt;span class="nt"&gt;-cs&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [arch=amd64 signed-by=/etc/apt/keyrings/microsoft.gpg] https://packages.microsoft.com/repos/azure-cli/ &lt;/span&gt;&lt;span class="nv"&gt;$AZ_DIST&lt;/span&gt;&lt;span class="s2"&gt; main"&lt;/span&gt; | &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/azure-cli.list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install &lt;/span&gt;azure-cli &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install SqlPackage
&lt;/h2&gt;

&lt;p&gt;To import the backup bacpac file that we're going to download from Azure Blob Storage, we will need to install the &lt;code&gt;sqlpackage&lt;/code&gt; utility.&lt;br&gt;
This utility is used to import and export bacpac files to and from SQL Server. The utility has &lt;a href="https://learn.microsoft.com/en-us/sql/tools/sqlpackage/sqlpackage-download?view=sql-server-ver16#supported-operating-systems" rel="noopener noreferrer"&gt;evergreen links&lt;/a&gt;&lt;br&gt;
available, so we can use the link to download the latest version of the utility.&lt;/p&gt;

&lt;p&gt;Add the following lines to the end of the &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install SQLPackage for Linux and make it executable&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; sqlpackage.zip https://aka.ms/sqlpackage-linux &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; unzip &lt;span class="nt"&gt;-qq&lt;/span&gt; sqlpackage.zip &lt;span class="nt"&gt;-d&lt;/span&gt; /sql/sqlpackage &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /sql/sqlpackage/sqlpackage &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm &lt;/span&gt;sqlpackage.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above lines download the latest version of the &lt;code&gt;sqlpackage&lt;/code&gt; utility from Microsoft's website, unzip it, make it executable, and then remove the zip file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entrypoint
&lt;/h2&gt;

&lt;p&gt;Finally, we need to add our entrypoint script to the container. This script will be run when the container starts, and it&lt;br&gt;
will perform all the necessary steps to download the latest backup from Azure Blob Storage and restore it to the local SQL Server instance.&lt;/p&gt;

&lt;p&gt;Add the following lines to the end of the &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Switch back to mssql user&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; mssql&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 1433&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; /bin/bash ./entrypoint.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to switch back to the &lt;code&gt;mssql&lt;/code&gt; user to run the SQL Server process, so we make use of the &lt;code&gt;USER&lt;/code&gt; command to do this.&lt;br&gt;
The &lt;code&gt;EXPOSE&lt;/code&gt; command tells Docker that the container listens on the specified network ports at runtime.&lt;br&gt;
The &lt;code&gt;CMD&lt;/code&gt; command specifies the command that will be run when the container starts. In this case, we are running the &lt;code&gt;entrypoint.sh&lt;/code&gt; script.&lt;/p&gt;

&lt;p&gt;We will make some final changes to our &lt;code&gt;Dockerfile&lt;/code&gt; later, but for now, save the file and close it.&lt;/p&gt;
&lt;h1&gt;
  
  
  The Scripts
&lt;/h1&gt;

&lt;p&gt;As we alluded to at the end of our &lt;code&gt;Dockerfile&lt;/code&gt;, we need to create an &lt;code&gt;entrypoint.sh&lt;/code&gt; script that will be run when the container starts.&lt;br&gt;
Since this container is based on the official Microsoft SQL Server Docker image, we need to ensure the original entrypoint is also run&lt;br&gt;
alongside our custom entrypoint script. To do this, we need to create an additional script that we will call in our &lt;code&gt;entrypoint.sh&lt;/code&gt; script.&lt;/p&gt;
&lt;h2&gt;
  
  
  entrypoint.sh
&lt;/h2&gt;

&lt;p&gt;Create two new files in the same directory as your &lt;code&gt;Dockerfile&lt;/code&gt; called &lt;code&gt;entrypoint.sh&lt;/code&gt; and &lt;code&gt;initialize-database-and-jobs.sh&lt;/code&gt;.&lt;br&gt;
Add the following content to &lt;code&gt;entrypoint.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
/sql/initialize-database-and-jobs.sh &amp;amp; /opt/mssql/bin/sqlservr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll note that we are running the &lt;code&gt;initialize-database-and-jobs.sh&lt;/code&gt; script in the background and then starting the SQL Server process.&lt;br&gt;
This &lt;code&gt;&amp;amp;&lt;/code&gt; syntax is necessary to ensure that the original entrypoint script is also run without the docker container exiting immediately after&lt;br&gt;
the script completes.&lt;/p&gt;
&lt;h2&gt;
  
  
  initialize-database-and-jobs.sh
&lt;/h2&gt;

&lt;p&gt;Add the following content to &lt;code&gt;initialize-database-and-jobs.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# wait 30 seconds for SQL Server to start up&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting for SQL Server to start"&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;30s

&lt;span class="c"&gt;# Download the bacpac file from the Azure Blob Storage&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Downloading bacpac file from Azure Blob Storage"&lt;/span&gt;
bash /sql/download-latest.sh &lt;span class="nv"&gt;$ACCOUNT_NAME&lt;/span&gt; &lt;span class="nv"&gt;$ACCOUNT_KEY&lt;/span&gt; &lt;span class="nv"&gt;$CONTAINER_NAME&lt;/span&gt; /sql/backup.bacpac
&lt;span class="nv"&gt;backupJob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backupJob&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Successfully downloaded bacpac file from Azure Blob Storage!"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Enabling SQL Server authentication..."&lt;/span&gt;
    /opt/mssql-tools/bin/sqlcmd &lt;span class="nt"&gt;-S&lt;/span&gt; localhost &lt;span class="nt"&gt;-U&lt;/span&gt; sa &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nv"&gt;$MSSQL_SA_PASSWORD&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; master &lt;span class="nt"&gt;-i&lt;/span&gt; /sql/enable-authentication.sql
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SQL Server authentication enabled. Waiting for 10 seconds before importing the bacpac file..."&lt;/span&gt;
    &lt;span class="nb"&gt;sleep &lt;/span&gt;10s

    &lt;span class="c"&gt;# Import the bacpac file into the SQL Server&lt;/span&gt;
    /sql/sqlpackage/sqlpackage /a:import /sf:/sql/backup.bacpac /tsn:localhost,1433 /tdn:&lt;span class="nv"&gt;$DATABASE_NAME&lt;/span&gt; /tu:sa /tp:&lt;span class="nv"&gt;$MSSQL_SA_PASSWORD&lt;/span&gt; /ttsc:True

    &lt;span class="c"&gt;# Set up 4am CRON job to re-import the database&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CRON_SCHEDULE&lt;/span&gt;&lt;span class="s2"&gt; /bin/bash /sql/reimport-database.sh"&lt;/span&gt; | crontab -
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"CRON job set up successfully"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Failed to download bacpac file from Azure Blob Storage"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This short script does quite a few operations. First, it's recommended to wait for SQL Server to start up before attempting to connect to it.&lt;br&gt;
We set up a 30-second timer to wait for SQL Server to start. We then download the latest backup from Azure Blob Storage using the &lt;code&gt;download-latest.sh&lt;/code&gt; script.&lt;br&gt;
If the download is successful, we use the built-in &lt;code&gt;sqlcmd&lt;/code&gt; utility to enable SQL Server authentication. We then wait for 10 seconds to ensure that the&lt;br&gt;
SQL Server has stabilized. We then use the &lt;code&gt;sqlpackage&lt;/code&gt; utility to import the bacpac file into the SQL Server. Finally, we set up a CRON job to run the&lt;br&gt;
&lt;code&gt;reimport-database.sh&lt;/code&gt; script at a frequency specified by the &lt;code&gt;CRON_SCHEDULE&lt;/code&gt; environment variable. We then exit the script with a success code.&lt;/p&gt;

&lt;p&gt;We need to create the &lt;code&gt;download-latest.sh&lt;/code&gt; and &lt;code&gt;reimport-database.sh&lt;/code&gt; scripts that are called in the &lt;code&gt;initialize-database-and-jobs.sh&lt;/code&gt; script.&lt;/p&gt;
&lt;h2&gt;
  
  
  download-latest.sh
&lt;/h2&gt;

&lt;p&gt;Create a new file called &lt;code&gt;download-latest.sh&lt;/code&gt; and add the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Description: This script downloads the latest backup from an Azure Storage Account&lt;/span&gt;
&lt;span class="c"&gt;# Usage: bash DownloadLatest.sh &amp;lt;storageAccountName&amp;gt; &amp;lt;storageAccountKey&amp;gt; &amp;lt;containerName&amp;gt; &amp;lt;localPath&amp;gt;&lt;/span&gt;

&lt;span class="nv"&gt;accountName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;accountKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
&lt;span class="nv"&gt;containerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;
&lt;span class="nv"&gt;localPath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;4&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="s2"&gt;"./backup.bacpac"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Get the name of the latest blob&lt;/span&gt;
&lt;span class="nv"&gt;firstBlob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az storage blob list &lt;span class="nt"&gt;--account-key&lt;/span&gt; &lt;span class="nv"&gt;$accountKey&lt;/span&gt; &lt;span class="nt"&gt;--account-name&lt;/span&gt; &lt;span class="nv"&gt;$accountName&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nv"&gt;$containerName&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"[?properties.lastModified!=null]|[?ends_with(name, '.bacpac')]|[0].name"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Check if $firstBlob is not null (i.e., there are blobs found)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$firstBlob&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;az storage blob download &lt;span class="nt"&gt;--account-key&lt;/span&gt; &lt;span class="nv"&gt;$accountKey&lt;/span&gt; &lt;span class="nt"&gt;--account-name&lt;/span&gt; &lt;span class="nv"&gt;$accountName&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nv"&gt;$containerName&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$firstBlob&lt;/span&gt; &lt;span class="nt"&gt;--file&lt;/span&gt; &lt;span class="nv"&gt;$localPath&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; none
    &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Azure CLI lets us write queries to filter the results of the &lt;code&gt;az storage blob list&lt;/code&gt; command. The queries are written in&lt;br&gt;
&lt;a href="https://jmespath.org/" rel="noopener noreferrer"&gt;JMESPath&lt;/a&gt;, which is a query language for JSON. In this case, we are filtering the results to only include blobs that end with the&lt;br&gt;
&lt;code&gt;.bacpac&lt;/code&gt; extension and then selecting the first one as ordered by the &lt;code&gt;lastModified&lt;/code&gt; property. If there are no blobs found, the script exits with a failure code.&lt;br&gt;
If we find a blob, we download it to the local path specified by the &lt;code&gt;localPath&lt;/code&gt; variable.&lt;/p&gt;
&lt;h2&gt;
  
  
  enable-authentication.sql
&lt;/h2&gt;

&lt;p&gt;Create a new file called &lt;code&gt;enable-authentication.sql&lt;/code&gt; and add the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;sp_configure&lt;/span&gt; &lt;span class="s1"&gt;'contained database authentication'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;span class="n"&gt;RECONFIGURE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script enables contained database authentication in the SQL Server instance. This is necessary to allow the &lt;code&gt;sa&lt;/code&gt; user to authenticate to the database&lt;br&gt;
when importing the bacpac file.&lt;/p&gt;
&lt;h2&gt;
  
  
  reimport-database.sh
&lt;/h2&gt;

&lt;p&gt;Create a new file called &lt;code&gt;reimport-database.sh&lt;/code&gt; and add the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Downloading bacpac file from Azure Blob Storage"&lt;/span&gt;
bash /sql/download-latest.sh &lt;span class="nv"&gt;$ACCOUNT_NAME&lt;/span&gt; &lt;span class="nv"&gt;$ACCOUNT_KEY&lt;/span&gt; &lt;span class="nv"&gt;$CONTAINER_NAME&lt;/span&gt; /sql/backup.bacpac
&lt;span class="nv"&gt;backupJob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$backupJob&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Successfully downloaded bacpac file from Azure Blob Storage!"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Kill all connections to the database"&lt;/span&gt;
    /opt/mssql-tools/bin/sqlcmd &lt;span class="nt"&gt;-S&lt;/span&gt; localhost &lt;span class="nt"&gt;-U&lt;/span&gt; sa &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nv"&gt;$MSSQL_SA_PASSWORD&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; master &lt;span class="nt"&gt;-i&lt;/span&gt; /sql/kill-all-connections.sql
    &lt;span class="nv"&gt;databaseName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_NAME&lt;/span&gt;
    &lt;span class="nv"&gt;existingDatabaseName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;databaseName&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Renaming existing database to &lt;/span&gt;&lt;span class="nv"&gt;$existingDatabaseName&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    /opt/mssql-tools/bin/sqlcmd &lt;span class="nt"&gt;-S&lt;/span&gt; localhost &lt;span class="nt"&gt;-U&lt;/span&gt; sa &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nv"&gt;$MSSQL_SA_PASSWORD&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; master &lt;span class="nt"&gt;-Q&lt;/span&gt; &lt;span class="s2"&gt;"ALTER DATABASE &lt;/span&gt;&lt;span class="nv"&gt;$databaseName&lt;/span&gt;&lt;span class="s2"&gt; MODIFY NAME = &lt;/span&gt;&lt;span class="nv"&gt;$existingDatabaseName&lt;/span&gt;&lt;span class="s2"&gt;;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Renamed existing database to &lt;/span&gt;&lt;span class="nv"&gt;$existingDatabaseName&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Importing bacpac file into the SQL Server"&lt;/span&gt;
    /sql/sqlpackage/sqlpackage /a:import /sf:/sql/backup.bacpac /tsn:localhost,1433 /tdn:&lt;span class="nv"&gt;$DATABASE_NAME&lt;/span&gt; /tu:sa /tp:&lt;span class="nv"&gt;$MSSQL_SA_PASSWORD&lt;/span&gt; /ttsc:True
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Failed to download bacpac file from Azure Blob Storage"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll notice that this script is very similar to the &lt;code&gt;initialize-database-and-jobs.sh&lt;/code&gt; script. The main difference is that we are renaming the existing database&lt;br&gt;
before importing the new bacpac file. This is necessary because the &lt;code&gt;sqlpackage&lt;/code&gt; utility does not support overwriting an existing database.&lt;br&gt;
We also need to kill all connections to the database before renaming it. We do this by running a SQL script that we will create called &lt;code&gt;kill-all-connections.sql&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  kill-all-connections.sql
&lt;/h2&gt;

&lt;p&gt;Create a new file called &lt;code&gt;kill-all-connections.sql&lt;/code&gt; and add the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- kill all connections to the database&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;killCommand&lt;/span&gt; &lt;span class="n"&gt;NVARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;killCommand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;killCommand&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;'KILL '&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spid&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;';'&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sysprocesses&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;dbid&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;EXEC&lt;/span&gt; &lt;span class="n"&gt;sp_executesql&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;killCommand&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As its name suggests, this script kills all connections to the database. This is necessary to ensure that we can rename the database without any active connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Directory Structure and Dockerfile Changes
&lt;/h2&gt;

&lt;p&gt;If everything went to plan, your directory should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── Dockerfile
├── entrypoint.sh
├── initialize-database-and-jobs.sh
├── download-latest.sh
├── enable-authentication.sql
├── reimport-database.sh
├── kill-all-connections.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now before we can build the Docker container, we need to make a few final changes to our &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;br&gt;
We need to copy all the files from our local machine to the container and set the correct permissions on the scripts.&lt;br&gt;
Add the following lines to your &lt;code&gt;Dockerfile&lt;/code&gt; directly after the &lt;code&gt;USER root&lt;/code&gt; line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; /home/mssql &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chown &lt;/span&gt;mssql /home/mssql &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /sql/initialize-database-and-jobs.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /sql/entrypoint.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /sql/download-latest.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;mkdir&lt;/code&gt; command creates a new directory in the container and the &lt;code&gt;chown&lt;/code&gt; command changes the owner of the directory to the &lt;code&gt;mssql&lt;/code&gt; user.&lt;br&gt;
We then set the correct permissions on the scripts using the &lt;code&gt;chmod&lt;/code&gt; command. Save the &lt;code&gt;Dockerfile&lt;/code&gt; and close it.&lt;/p&gt;
&lt;h1&gt;
  
  
  Building the Docker Container
&lt;/h1&gt;

&lt;p&gt;Now that we have all the necessary files, we can build the Docker container. Open a terminal and navigate to the directory where your &lt;code&gt;Dockerfile&lt;/code&gt; is located.&lt;br&gt;
Run the following command to build the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; azure-local-database-refresh &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command will build the container using the &lt;code&gt;Dockerfile&lt;/code&gt; in the current directory and tag the container with the name &lt;code&gt;azure-local-database-refresh&lt;/code&gt;.&lt;br&gt;
The build process may take a few minutes to complete. Once it's done, you can run the container using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ACCOUNT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;storageAccountName&amp;gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ACCOUNT_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;storageAccountKey&amp;gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;CONTAINER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;containerName&amp;gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DATABASE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;MyDatabase &lt;span class="nt"&gt;-p&lt;/span&gt; 1433:1433 &lt;span class="nt"&gt;-it&lt;/span&gt;  azure-local-database-refresh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;storageAccountName&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;storageAccountKey&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;containerName&amp;gt;&lt;/code&gt; with the appropriate values for your Azure Blob Storage account. The &lt;code&gt;-p&lt;/code&gt; flag maps port 1433 of the container to port 1433 of your local machine.&lt;br&gt;
This allows you to connect to the SQL Server instance running in the container from your local machine. The &lt;code&gt;-it&lt;/code&gt; flag runs the container in interactive mode, which allows you to see the output of the container in your terminal.&lt;/p&gt;

&lt;p&gt;You should see the output of the container in your terminal. If everything is working correctly, you should see messages indicating that the bacpac file has been downloaded and imported into the SQL Server instance. The import&lt;br&gt;
process can take a several minutes to complete, depending on the size of the database. Once the import process is complete, you should see a message indicating that the CRON job has been set up successfully.&lt;/p&gt;
&lt;h1&gt;
  
  
  Testing the Container
&lt;/h1&gt;

&lt;p&gt;Now that we have our container up and running, we can test it by connecting to the SQL Server instance and verifying that the database has been restored.&lt;br&gt;
Open a new terminal and run the following command to connect to the SQL Server instance running in the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;containerId&amp;gt; /opt/mssql-tools/bin/sqlcmd &lt;span class="nt"&gt;-S&lt;/span&gt; localhost &lt;span class="nt"&gt;-U&lt;/span&gt; sa &lt;span class="nt"&gt;-P&lt;/span&gt; yourStrong&lt;span class="o"&gt;(!)&lt;/span&gt;Password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;containerId&amp;gt;&lt;/code&gt; with the ID of your container. This command will open an interactive SQL Server prompt. Run the following command to verify that the database has been restored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;databases&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything went to plan you should see the name of your database in the output. You can also run queries against the database to verify that the data has been restored correctly.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;In this article, we created a Docker container that automates the process of downloading the latest backup of a production database from Azure Blob Storage and restoring it to a local SQL Server instance.&lt;br&gt;
We used a combination of bash scripts and Docker to create a portable and easy-to-use solution that can be run on any machine that has Docker installed. This solution can be used to provide teams with access&lt;br&gt;
to the latest production data for development, testing, reporting, data analysis, or other internal uses. The container can be run on a schedule using a CRON job to ensure that the data is always up-to-date. This solution can be&lt;br&gt;
easily extended to support other databases and cloud storage providers.&lt;/p&gt;

&lt;p&gt;The code for this solution is available on &lt;a href="https://github.com/JerrettDavis/az-bacpac-blob-mssql-importer" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;br&gt;
This docker container is also available on &lt;a href="https://hub.docker.com/r/jdhproductions/az-bacpac-blob-mssql-importer" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>bash</category>
      <category>azure</category>
    </item>
  </channel>
</rss>
