{"slug": "vibe-engineering-solving-small-cross-cutting-concerns", "title": "Vibe Engineering: Solving Small Cross-Cutting Concerns", "summary": "A developer with experience in AI across oil & gas and healthcare describes building a high-end PC to run local LLMs and diffusion models, and joining a new company in late 2025 to tackle complex codebases. The engineer uses AI coding tools as interactive assistants while performing most work manually.", "body_md": "This article was originally published on\n\n[my blog].\n\nI'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 & 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.\n\nAfter 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.\n\nA 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.\n\nAs 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.\n\nAround 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.\n\nI 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.\n\nThen 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.\n\nTo 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.\n\nSo I got to chomping.\n\nBite 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.\n\nThe 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.\n\nThere 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.\n\nLittered 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.\n\nAnd you start to make sense of what you're looking at.\n\nNot 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.\n\nSlowly 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...\n\nArchitectural 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, *Design Patterns* 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.\n\nDespite 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.\n\nFor better or worse, I was never instilled with such predeterminations.\n\nEver 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:\n\nAnyone 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.\n\nIt'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.\n\nWhat 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.\n\nI want to linger on \"declarative\" for a moment. It's become central to how I think about code quality.\n\nDeclarative code describes *what* should happen, not *how* it happens. It reads like a specification rather than a set of instructions. It's flat rather than nested. Composable rather than procedural.\n\nConsider the difference:\n\n``` js\n// Imperative: a sequence of instructions\nvar user = GetUser(id);\nif (user != null)\n{\n    var permissions = GetPermissions(user.RoleId);\n    if (permissions != null && permissions.Contains(\"admin\"))\n    {\n        var report = GenerateReport(user);\n        if (report != null)\n        {\n            await SendEmail(user.Email, report);\n            LogSuccess(user.Id);\n        }\n        else\n        {\n            LogFailure(user.Id, \"Report generation failed\");\n        }\n    }\n}\n// Declarative: a description of what should happen\nawait Given(userId)\n    .Select(GetUser)\n    .Where(user => user.HasPermission(\"admin\"))\n    .Select(GenerateReport)\n    .Match(\n        some: report => SendEmail(report).Then(LogSuccess),\n        none: () => LogFailure(\"Report generation failed\"));\n```\n\nThe 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.\n\nThis 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.\n\nPackage 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.\n\nHow can one begin to design a system if they don't know what they're building?\n\nToo 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.\n\nExperimentation is great. It's one of the foundations of our field. But experimentation is not for production work.\n\nThat'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.\n\nWhen 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.\n\nI'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.\n\nBut 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.\n\nI'm prone to overusing metaphors. No exception here: I never want to do the digital equivalent of drywall.\n\nPlacing, 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.\n\nThing 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.\n\nWhen 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.\n\nAnd this is where the modern era offers us something genuinely new.\n\nAI agents have amazing potential. But like overzealous, over-studious summer interns, they lack refinement, direction, and context.\n\nThat 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.\n\nOnce 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.\n\nYou 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.\n\nThe 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.\n\nI keep coming back to the intern metaphor because it's genuinely the most apt comparison I've found.\n\nA 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 `tbl_Users_OLD_DONT_USE_2019`\n\n. They're capable. They're eager. They will absolutely demolish your carefully maintained naming conventions if you don't give them explicit guidance.\n\nAgents operate the same way.\n\nThey'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 `ICommandHandler<T>`\n\ninstead of `IHandler<T>`\n\nbecause someone made that decision in 2017 and it stuck. They don't know that the `Utils`\n\nfolder 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 `GetReportData`\n\nstored procedure because he's been calling it directly from Excel for seven years.\n\nContext is everything. And context is precisely what agents lack until you give it to them.\n\nThere's a fundamental distinction in how you can leverage agents that took me longer to articulate than I'd like to admit.\n\nYou want agents to build tools that you validate. Not to make decisions that you trust.\n\nThe 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.\n\nBut when an agent builds a tool? The dynamic shifts entirely.\n\nThe 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.\n\nThis 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.\n\nCompare 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.\n\nWith 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.\n\nThis 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.\n\nThe mantra I've adopted: \"Agents build tools. Tools do work. I validate tools.\"\n\nThe 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.\n\nFor 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.\n\nAnd 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.\n\nConsider 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.\n\nCompare 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.\n\nI'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.\n\nThe workflow looks something like this:\n\nEach 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.\n\nLet's dig into each of these stages.\n\nThe 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.\n\nAgents 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.\n\nBut 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.\n\nIf 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.\n\nFor 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.\n\nThe 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:\n\n```\nAnalyze the following repositories for cross-cutting concerns.\nA cross-cutting concern is any functionality that:\n1. Appears in multiple applications or services\n2. Is not core business logic\n3. Could reasonably be extracted into a shared library\n4. Currently has inconsistent implementations across the codebase\n\nFocus specifically on:\n- Logging and observability patterns\n- Configuration and options binding\n- Error handling and exception management\n- HTTP client usage and resilience patterns\n- Caching strategies\n- Validation approaches\n- Mapping and transformation utilities\n\nFor each concern you identify, provide:\n- Where it appears (specific files/projects)\n- How implementations vary\n- What problems the inconsistency causes\n- A preliminary assessment of extraction complexity\n```\n\nThe 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 `HttpClient`\n\n(with all the socket exhaustion problems that entails). Some used `IHttpClientFactory`\n\ncorrectly. 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 `HttpClient`\n\nwrapped in a `lock`\n\nstatement \"for thread safety.\"\n\nNone 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.\n\nOnce 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.\n\nFor the HTTP client example, the enumeration might look like:\n\n```\nHTTP Client Implementations Inventory:\n\n1. ServiceA/Infrastructure/HttpClientWrapper.cs\n   - Pattern: Static HttpClient with manual DNS refresh\n   - Retry logic: None\n   - Timeout: 30 seconds (hardcoded)\n   - Dependencies: None\n\n2. ServiceB/Clients/ExternalApiClient.cs\n   - Pattern: IHttpClientFactory via constructor injection\n   - Retry logic: Polly with 3 retries, exponential backoff\n   - Timeout: Configured via appsettings.json\n   - Dependencies: Polly, Microsoft.Extensions.Http\n\n3. ServiceC/Utilities/ApiHelper.cs\n   - Pattern: New HttpClient per request (!)\n   - Retry logic: Manual try-catch with Thread.Sleep\n   - Timeout: 100 seconds (default)\n   - Dependencies: None\n\n[... and so on for each occurrence ...]\n```\n\nThis inventory becomes the foundation for the next phase. You can't plan an extraction without knowing what you're extracting from.\n\nNot 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.\n\nI typically categorize concerns into three buckets:\n\n**Low complexity**: 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.\n\n**Medium complexity**: 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.\n\n**High complexity**: 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.\n\nAgents 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.\n\nThe 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:\n\n```\n# Cross-Cutting Concerns Registry\n\n## HTTP Client Management\n- **Status**: Identified, awaiting prioritization\n- **Complexity**: Medium\n- **Occurrences**: 12 services\n- **Primary variations**: Retry policies, timeout configurations, authentication handling\n- **Proposed solution**: Shared library with configurable policies\n- **Estimated effort**: 2 weeks extraction, 4 weeks migration\n\n## Structured Logging\n- **Status**: In progress\n- **Complexity**: Low\n- **Occurrences**: 23 services\n- **Primary variations**: Log format, correlation ID propagation\n- **Proposed solution**: Thin wrapper over Serilog with standard enrichers\n- **Estimated effort**: 1 week extraction, 2 weeks migration\n\n[... additional concerns ...]\n```\n\nThis 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.\n\nOnce 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.\n\nFor 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.\n\nAgents 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.\n\nA typical ADR structure might look like:\n\n```\n# ADR-007: Standardize HTTP Client Management\n\n## Status\nProposed\n\n## Context\nOur service portfolio currently contains 12 different approaches to HTTP\nclient management. This inconsistency has led to:\n\n- Production incidents due to socket exhaustion (ServiceC, ServiceF)\n- Inconsistent retry behavior causing cascading failures\n- Security vulnerabilities from improper certificate validation\n- Developer confusion and duplicated effort\n\nAnalysis of the current implementations reveals three main categories:\n[detailed breakdown follows]\n\n## Decision\nWe will create a shared library (`Company.Http.Resilience`) that provides:\n\n1. Pre-configured HttpClient instances via IHttpClientFactory\n2. Standard retry policies with configurable parameters\n3. Circuit breaker integration\n4. Consistent timeout handling\n5. Structured logging of HTTP operations\n6. Health check integration\n\n## Consequences\n\n### Positive\n- Consistent behavior across all services\n- Reduced incident rate from HTTP-related issues\n- Faster onboarding for new developers\n- Single place to update security configurations\n\n### Negative\n- Migration effort required for existing services\n- Learning curve for teams unfamiliar with new patterns\n- Potential for breaking changes if not carefully managed\n\n### Neutral\n- Requires ongoing maintenance of shared library\n- Need to establish governance for library updates\n\n## Migration Strategy\n[phased approach details]\n```\n\nOne 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.\n\nAgents can help generate multiple views from the same underlying data:\n\n**Executive summary** (for leadership):\n\n```\nWe've identified 12 inconsistent HTTP client implementations across our\nservices. This inconsistency contributed to 3 production incidents last\nquarter and consumes approximately 40 developer-hours per month in\ndebugging and maintenance. A 2-week investment to create a standardized\nlibrary would eliminate this overhead and reduce incident risk.\n```\n\n**Technical summary** (for architecture review):\n\n```\nCross-cutting concern: HTTP client management\nCurrent state: 12 implementations, 4 distinct patterns\nRisk level: High (socket exhaustion, retry storms)\nProposed solution: Shared library with IHttpClientFactory, Polly policies\nEffort estimate: 80 hours extraction, 160 hours migration\nDependencies: Microsoft.Extensions.Http, Polly\n```\n\n**Developer guide** (for implementation teams):\n\n```\n# Migrating to Company.Http.Resilience\n\n## Why are we doing this?\n[brief context]\n\n## What changes for me?\n[concrete before/after examples]\n\n## How do I migrate?\n[step-by-step instructions]\n\n## What if I need custom behavior?\n[extension points and configuration options]\n```\n\nI'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:\n\nEach 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.\n\nDocumentation 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.\n\nThis 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:\n\n```\nReview ADR-007 against the current state of our HTTP client implementations.\nIdentify any drift between the documented decision and actual implementation.\nFlag any new services that haven't adopted the standard library.\nSuggest updates to the ADR if the decision should be revised.\n```\n\nThis 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.\n\nThis 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.\n\nPlanning is collaborative, but the human must drive.\n\nThe first planning decision is scope: what exactly are you extracting, and what are you leaving behind?\n\nThis 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.\n\nThe antidote is aggressive scoping. For each cross-cutting concern, define:\n\n**What's in scope:**\n\n**What's explicitly out of scope:**\n\nWrite these boundaries down. Put them in the ADR. Refer back to them when you (or your agent) start gold-plating.\n\nBefore 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.\n\nFor a shared library, the contract includes:\n\nI 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.\n\nWhen 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.\n\nCompare these two approaches to configuring an HTTP client:\n\n``` js\n// Nested/Constructor-heavy approach\nvar client = new ResilientHttpClient(\n    new HttpClientOptions(\n        baseAddress: \"https://api.example.com\",\n        timeout: TimeSpan.FromSeconds(30),\n        retryPolicy: new RetryPolicy(\n            maxRetries: 3,\n            backoffType: BackoffType.Exponential,\n            initialDelay: TimeSpan.FromMilliseconds(100)\n        ),\n        circuitBreaker: new CircuitBreakerOptions(\n            failureThreshold: 5,\n            samplingDuration: TimeSpan.FromMinutes(1),\n            breakDuration: TimeSpan.FromSeconds(30)\n        )\n    )\n);\njs\n// Fluent approach\nvar client = ResilientHttpClient\n    .Create(\"https://api.example.com\")\n    .WithTimeout(TimeSpan.FromSeconds(30))\n    .WithRetry(retry => retry\n        .MaxAttempts(3)\n        .ExponentialBackoff()\n        .InitialDelay(TimeSpan.FromMilliseconds(100)))\n    .WithCircuitBreaker(breaker => breaker\n        .TripAfter(5).FailuresIn(TimeSpan.FromMinutes(1))\n        .StayOpenFor(TimeSpan.FromSeconds(30)))\n    .Build();\n```\n\nThe 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.\n\nThis 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 *use* a fluent API, the resulting code is more likely to be correct because each step is self-documenting.\n\nI've built entire libraries around this philosophy. [PatternKit](https://github.com/JerrettDavis/PatternKit) provides fluent implementations of Gang of Four patterns precisely because the traditional examples are unnecessarily verbose and nested. [TinyBDD](https://github.com/JerrettDavis/TinyBDD) uses a fluent Given/When/Then syntax because test scenarios should read like specifications, not like procedural code.\n\nWhen planning your extracted libraries, default to fluent. Your future self (and your agents) will thank you.\n\nBut 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.\n\nThis 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.\n\n```\n// This is what we WANT the consuming code to look like\n\npublic class MyService\n{\n    private readonly IResilientHttpClient _http;\n\n    public MyService(IResilientHttpClient http)\n    {\n        _http = http;\n    }\n\n    public async Task<CustomerData> GetCustomerAsync(string customerId)\n    {\n        // Simple case: just make the call, resilience is handled for us\n        return await _http.GetAsync<CustomerData>($\"/customers/{customerId}\");\n    }\n\n    public async Task<OrderResult> PlaceOrderAsync(Order order)\n    {\n        // Custom configuration for this specific call\n        var options = new HttpCallOptions\n        {\n            Timeout = TimeSpan.FromSeconds(60),\n            RetryPolicy = RetryPolicy.None  // Orders shouldn't auto-retry\n        };\n\n        return await _http.PostAsync<OrderResult>(\"/orders\", order, options);\n    }\n}\n```\n\nAnd the registration:\n\n``` js\n// In Startup or Program.cs\nservices.AddResilientHttpClient(options =>\n{\n    options.BaseAddress = configuration[\"ApiBaseUrl\"];\n    options.DefaultTimeout = TimeSpan.FromSeconds(30);\n    options.RetryPolicy = new RetryPolicyOptions\n    {\n        MaxRetries = 3,\n        BackoffType = BackoffType.Exponential\n    };\n    options.CircuitBreaker = new CircuitBreakerOptions\n    {\n        FailureThreshold = 5,\n        SamplingDuration = TimeSpan.FromMinutes(1),\n        BreakDuration = TimeSpan.FromSeconds(30)\n    };\n});\n```\n\nThis \"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.\n\nWhile the human defines the desired interface, agents can be invaluable for exploring the design space. Give them the proposed interface and ask:\n\n```\nReview this proposed interface for a resilient HTTP client library.\nConsider:\n- Usability: Is this intuitive for developers to use?\n- Flexibility: Can it accommodate the variations we found in our analysis?\n- Testability: Can consuming code easily mock or stub this?\n- Consistency: Does it follow .NET conventions and patterns?\n- Evolution: Can we extend this without breaking changes?\n\nIdentify potential issues and suggest alternatives.\n```\n\nThe agent might surface considerations you hadn't thought of:\n\n`RetryPolicy.None`\n\noption might be confusing; consider `RetryPolicy.Disabled`\n\nfor clarity.\"`HttpCallOptions`\n\nclass; an interface would improve testability.\"`CircuitBreaker.Aggressive`\n\nand `CircuitBreaker.Conservative`\n\n.\"Not all suggestions will be good, but the exploration is valuable. It's like having a code review before the code exists.\n\nOnce 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:\n\n```\n[Feature(\"Resilient HTTP Client\")]\npublic class ResilientHttpClientScenarios : TinyBddXunitBase\n{\n    [Scenario(\"Successful request returns deserialized response\")]\n    [Fact]\n    public async Task SuccessfulRequest()\n    {\n        await Given(\"a configured resilient HTTP client\", () =>\n                CreateTestClient(respondWith: new CustomerData { Id = \"123\", Name = \"Test\" }))\n            .When(\"making a GET request\", client =>\n                client.GetAsync<CustomerData>(\"/customers/123\"))\n            .Then(\"the response is deserialized correctly\", customer =>\n                customer.Id == \"123\" && customer.Name == \"Test\")\n            .AssertPassed();\n    }\n\n    [Scenario(\"Transient failure triggers retry\")]\n    [Fact]\n    public async Task TransientFailureRetry()\n    {\n        await Given(\"a client configured with 3 retries\", () =>\n                CreateTestClient(failTimes: 2, thenSucceed: true))\n            .When(\"making a request that fails twice then succeeds\", client =>\n                client.GetAsync<CustomerData>(\"/customers/123\"))\n            .Then(\"the request ultimately succeeds\", customer =>\n                customer != null)\n            .And(\"exactly 3 attempts were made\", () =>\n                GetAttemptCount() == 3)\n            .AssertPassed();\n    }\n\n    [Scenario(\"Circuit breaker opens after threshold\")]\n    [Fact]\n    public async Task CircuitBreakerOpens()\n    {\n        await Given(\"a client with circuit breaker (threshold: 3 failures)\", () =>\n                CreateTestClient(alwaysFail: true, circuitBreakerThreshold: 3))\n            .When(\"making 5 failing requests\", async client =>\n            {\n                for (int i = 0; i < 5; i++)\n                {\n                    try { await client.GetAsync<CustomerData>(\"/customers/123\"); }\n                    catch { /* expected */ }\n                }\n                return client;\n            })\n            .Then(\"the circuit breaker is open\", client =>\n                client.IsCircuitBreakerOpen)\n            .And(\"later requests fail fast without attempting\", async client =>\n            {\n                var sw = Stopwatch.StartNew();\n                try { await client.GetAsync<CustomerData>(\"/customers/123\"); }\n                catch { /* expected */ }\n                return sw.ElapsedMilliseconds < 100;  // Should fail immediately\n            })\n            .AssertPassed();\n    }\n}\n```\n\nThese 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).\n\nThe output of the planning phase should be a comprehensive specification that can be handed to an implementer (human or otherwise):\n\nWith 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.\n\nWith 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.\n\nThe implementation workflow follows a tight loop:\n\nThis 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.\n\nWhen prompting for implementations, I explicitly request flat, functional code. This isn't just stylistic preference; it directly impacts the quality of agent-generated code.\n\nAgents 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 `if`\n\nstatements inside `try`\n\nblocks inside `foreach`\n\nloops inside `while`\n\nconditions.\n\nCounter this by being explicit in your prompts:\n\n```\nImplement the retry logic for IResilientHttpClient.\n\nStyle requirements:\n- Prefer pure functions over stateful methods\n- Use early returns to avoid nesting\n- Chain operations rather than nesting them\n- Each method should do one thing\n- Avoid mutable state where possible\n- Use pattern matching over if/else chains\n\nExample of the style I want:\n[paste example of flat, functional code from your codebase]\n```\n\nThe difference in output quality is stark. With explicit style guidance, you get code like:\n\n```\npublic async Task<T> ExecuteWithRetryAsync<T>(\n    Func<Task<T>> operation,\n    RetryPolicy policy,\n    CancellationToken ct = default)\n{\n    return await policy.Attempts\n        .Select(attempt => TryExecute(operation, attempt, ct))\n        .FirstSuccessOrThrowAggregate();\n}\n\nprivate async Task<Result<T>> TryExecute<T>(\n    Func<Task<T>> operation,\n    int attempt,\n    CancellationToken ct)\n{\n    try\n    {\n        var result = await operation();\n        return Result.Success(result);\n    }\n    catch (Exception ex) when (IsTransient(ex))\n    {\n        await DelayForAttempt(attempt, ct);\n        return Result.Failure<T>(ex);\n    }\n}\n```\n\nWithout style guidance, you get:\n\n```\npublic async Task<T> ExecuteWithRetryAsync<T>(\n    Func<Task<T>> operation,\n    RetryPolicy policy,\n    CancellationToken ct = default)\n{\n    int attempts = 0;\n    List<Exception> exceptions = new List<Exception>();\n\n    while (attempts < policy.MaxAttempts)\n    {\n        try\n        {\n            return await operation();\n        }\n        catch (Exception ex)\n        {\n            if (IsTransient(ex))\n            {\n                exceptions.Add(ex);\n                attempts++;\n                if (attempts < policy.MaxAttempts)\n                {\n                    await Task.Delay(CalculateDelay(attempts), ct);\n                }\n            }\n            else\n            {\n                throw;\n            }\n        }\n    }\n\n    throw new AggregateException(exceptions);\n}\n```\n\nBoth 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 (`attempts`\n\n, `exceptions`\n\n), multiple exit points, and nested control flow.\n\nA typical prompting session might look like:\n\n```\nImplement the IResilientHttpClient interface according to this specification.\nStart with the basic request/response flow - don't worry about retry logic yet.\n\nInterface:\n[paste interface definition]\n\nScenario to satisfy:\n[paste the \"Successful request returns deserialized response\" scenario]\n\nConstraints:\n- Use System.Net.Http.HttpClient internally via IHttpClientFactory\n- Use System.Text.Json for serialization\n- Follow .NET naming conventions\n- Include XML documentation comments\n```\n\nThe agent produces an implementation. You run the scenario. It passes (or doesn't). You iterate.\n\nNot all scenarios are equally straightforward. The basic happy path is usually trivial. The edge cases are where implementations get interesting.\n\nFor complex scenarios like circuit breaker behavior, I've found it helpful to break the implementation into phases:\n\n**Phase 1: Core flow**\n\n**Phase 2: Retry logic**\n\n**Phase 3: Circuit breaker**\n\n**Phase 4: Observability**\n\nEach 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.\n\nAgent-generated code needs review. Always. Without exception.\n\nThe 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:\n\nI 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.\n\nThe agent can help with refinement too:\n\n```\nReview this implementation for potential issues:\n[paste code]\n\nSpecifically check for:\n- Thread safety concerns\n- Resource disposal issues\n- Potential for null reference exceptions\n- Adherence to .NET best practices\n\nSuggest improvements but don't rewrite the entire thing.\n```\n\nThis 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.\n\nConsider 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.\n\nThe prompt might look like:\n\n```\nCreate a C# source generator that:\n1. Finds all classes decorated with [Repository]\n2. Generates an IXxxRepository interface with standard CRUD methods\n3. Generates an XxxRepository implementation using our base class\n4. Generates extension methods for DI registration\n\nConventions to follow:\n- Interface goes in Abstractions namespace\n- Implementation goes in Infrastructure namespace\n- Use async/await throughout\n- Include XML documentation\n\nHere's an example of what the generated code should look like:\n[paste example of desired output]\n```\n\nThe 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:\n\n``` js\n[Fact]\npublic void Generator_Creates_Interface_For_Decorated_Class()\n{\n    var source = @\"\n        [Repository]\n        public class Customer { public int Id { get; set; } }\n    \";\n\n    var generated = RunGenerator(source);\n\n    generated.ShouldContain(\"public interface ICustomerRepository\");\n    generated.ShouldContain(\"Task<Customer?> GetByIdAsync(int id)\");\n}\n\n[Fact]\npublic void Generator_Follows_Namespace_Conventions()\n{\n    var source = @\"\n        namespace MyApp.Domain\n        {\n            [Repository]\n            public class Order { }\n        }\n    \";\n\n    var generated = RunGenerator(source);\n\n    generated.ShouldContain(\"namespace MyApp.Abstractions\");\n    generated.ShouldContain(\"namespace MyApp.Infrastructure\");\n}\n```\n\nNow 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.\n\nThis 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).\n\nThe same pattern applies to:\n\nEach 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.\n\nBDD 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.\n\nFor a shared library, I typically want:\n\n**Scenario tests** (BDD): Define the behavioral contract from the consumer's perspective\n\n**Unit tests**: Cover implementation details that scenarios don't reach (edge cases in internal methods, etc.)\n\n**Integration tests**: Verify behavior against real dependencies (actual HTTP servers, actual circuit breakers under load)\n\n**Performance tests**: Ensure the implementation doesn't introduce unacceptable overhead\n\nAgents 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:\n\n```\nGenerate unit tests for the RetryPolicy class.\nAchieve >90% code coverage.\nFocus on edge cases: zero retries, maximum retries, invalid configurations.\nUse xUnit and FluentAssertions.\n```\n\nIntegration 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.\n\nOnce the implementation is complete and all tests pass, the library needs to be packaged for consumption. This typically means:\n\nMost 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.\n\n```\nGenerate the NuGet package configuration for Company.Http.Resilience.\nInclude:\n- Package metadata (description, authors, license, repository URL)\n- Dependency specifications\n- README inclusion\n- Source Link configuration for debugging\n\nFollow the conventions used in our existing packages:\n[paste example .csproj]\n```\n\nThe output of the build phase is a functioning library with:\n\nAt 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.\n\nThis 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.\n\nSix 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.\n\nMaintenance is not glamorous, but it's where the investment in good architecture pays dividends (or doesn't).\n\nThe 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:\n\n**Proactive work**: Dependency updates, security patches, performance improvements\n\n**Reactive work**: Bug fixes, feature requests, compatibility updates\n\n**Communication**: Release notes, migration guides, deprecation warnings\n\nAgents can help with all of these, but they need context about what's changed and what matters.\n\nEvery library has dependencies, and dependencies evolve. At minimum, you need to:\n\nI use agents to help with dependency triage:\n\n```\nReview the following dependency updates for Company.Http.Resilience:\n\n- Microsoft.Extensions.Http: 8.0.0 -> 8.0.1 (patch)\n- Polly: 8.2.0 -> 8.3.0 (minor)\n- System.Text.Json: 8.0.0 -> 9.0.0 (major)\n\nFor each update:\n1. Summarize what changed (check release notes)\n2. Assess breaking change risk\n3. Recommend whether to update now, later, or skip\n4. Note any code changes required in our library\n```\n\nThe 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.\n\nConsumers 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.\n\nFor each request, ask:\n\nAgents can help evaluate requests against these criteria:\n\n```\nEvaluate this feature request for Company.Http.Resilience:\n\nRequest: Add support for request/response caching\n\nConsider:\n- Our stated scope: HTTP client resilience (retry, circuit breaker, timeout)\n- Caching is related but distinct from resilience\n- Adding caching would increase complexity significantly\n- Caching has its own configuration concerns (TTL, invalidation, storage)\n\nRecommendation: Accept, reject, or defer? Justify.\n```\n\nThe recommendation might be: \"Defer. Caching is related to HTTP concerns but orthogonal to resilience. Consider a separate `Company.Http.Caching`\n\nlibrary that composes with this one. If multiple consumers request it, prioritize the separate library.\"\n\nLibraries evolve through versions. Semantic versioning provides the framework (major.minor.patch), but the decisions about what goes into each version are human judgment calls.\n\nI maintain a version roadmap for each shared library:\n\n```\n# Company.Http.Resilience Version Roadmap\n\n## Current: 1.2.0\n- Retry with exponential backoff\n- Circuit breaker\n- Configurable timeouts\n- Structured logging\n\n## Planned: 1.3.0 (Q2 2026)\n- Request hedging (speculative execution)\n- Enhanced metrics (histograms, percentiles)\n- .NET 9 support\n\n## Planned: 2.0.0 (Q4 2026)\n- Breaking: Simplified configuration API\n- Breaking: Remove deprecated methods from 1.x\n- New: Native AOT support\n- New: gRPC resilience (separate package)\n\n## Backlog (unprioritized)\n- Request caching (may be separate library)\n- Custom serializers\n- Request signing\n```\n\nThis 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.\n\nDocumentation 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.\n\nPeriodic documentation audits are essential:\n\n```\nAudit the documentation for Company.Http.Resilience:\n\n1. Compare public API surface to documented features\n2. Identify undocumented public members\n3. Identify documented features that no longer exist\n4. Check that code examples compile against current version\n5. Verify links are not broken\n\nReport discrepancies and suggest fixes.\n```\n\nThis 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.\n\nRather than treating maintenance as ad-hoc work that happens when things break, establish a rhythm:\n\n**Weekly**: Dependency update review (automated alerts, manual triage)\n\n**Monthly**: Feature request review (batch similar requests, make decisions)\n\n**Quarterly**: Documentation audit, roadmap review\n\n**Yearly**: Major version planning, breaking change assessment\n\nThis 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.\n\nThe goal is to make maintenance a sustainable, predictable activity rather than a crisis response.\n\nWhen 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.\n\nWhat 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.\n\nThe 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.\n\nThe 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.\n\nHere's the key insight: these capabilities and limitations are complementary.\n\nAgents 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.\n\nVibe 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.\n\nAnd part of that vibe is aesthetic.\n\nI'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.\n\nWhen 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.\n\nSeveral months into this approach, a few lessons have crystallized.\n\n**Start small.** 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.\n\n**Build tools, not outputs.** 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.\n\n**Demand flat, declarative code.** 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.\n\n**Invest in specifications.** 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.\n\n**Trust but verify.** 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.\n\n**Design for validation.** 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.\"\n\n**Prefer fluent APIs.** 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.\n\n**Maintain the paper trail.** 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.\n\n**Establish rhythms.** 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.\n\nI'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.\n\nBut 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.\n\nThe 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.\n\nFor 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.\n\nAnd I've got an agent ready to help.\n\n*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 jerrettdavis.com or wherever you found this post.*\n\n*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.*", "url": "https://wpnews.pro/news/vibe-engineering-solving-small-cross-cutting-concerns", "canonical_source": "https://dev.to/jerrettdavis/vibe-engineering-solving-small-cross-cutting-concerns-35e6", "published_at": "2026-07-14 05:16:33+00:00", "updated_at": "2026-07-14 05:59:59.559183+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "generative-ai", "developer-tools"], "entities": ["OpenAI", "ChatGPT", "Ollama", "Stable Diffusion", "GeForce RTX 4090", "Intel i9 14900k"], "alternates": {"html": "https://wpnews.pro/news/vibe-engineering-solving-small-cross-cutting-concerns", "markdown": "https://wpnews.pro/news/vibe-engineering-solving-small-cross-cutting-concerns.md", "text": "https://wpnews.pro/news/vibe-engineering-solving-small-cross-cutting-concerns.txt", "jsonld": "https://wpnews.pro/news/vibe-engineering-solving-small-cross-cutting-concerns.jsonld"}}