My Blog

AI Software Development Patterns

Saturday, June 27, 2026 12:00 AM

It's something of a wonder, really

I closed out 2025 with a post talking about my feelings towards AI, with more questions than answers, and a mix of skepticism and curiosity.

Fast-forward half a year, and I doubt I could have predicted how much my habits and perspectives would have changed. I will save the full retrospective for December of this year, but I have to say, the culture and coverage of all this really moves at a breakneck pace! I'm also pleased to see that some of my predictions were roughly in the ballpark of what's happened this year so far as well.

✍ Though I will remind everyone, my blog posts remain hand-authored...for now.

Briefly, Vertical Slicing and Transaction Scripts

Prior to the current AI era, I'd been interested in applying vertical slicing, relaxing of DRY, transaction scripts and shallow structuring in dotnet projects. Especially as presented by Derek Comartin of CodeOpinion; Who I will always waste no opportunity to mention in my writings and conversations.

Vertical Slicing

The main thing with vertical slicing that I enjoy is how it's centered around grouping functionality according to business domain. It's absolutely an art form past a certain point, but getting started I've found really centers on resisting the urge to group namespaces, modules and files by their commonalities at a language or pattern-level.

So, instead of having a Models/ directory (and more importantly, corresponding namespace) at the toplevel of your projects, try one or both of:

  • Simply not having sub-namespaces at all, and keeping everything at the root
  • Creating a directory to represent the area of functionality in a tear-away fashion

One other neat mind-trick I like to use is to imagine two people working on the same codebase, but in different areas. What kind of filesystem structure would help ensure that their changesets stand the best chance of never colliding? How much easier could I make it so that when they merge their changes, the likelihood of a conflict is minimized?

Transaction Scripts

There might be some temptation to frame this as a technology concept, but it really isn't. Once most people sort out what transaction scripts are, they tend to think either "oh, I already do that!" or "I use services for logic."

Though I will say that they do defy some definition, transaction scripts are just "start at the top, and run to the end." It takes a deep familiarity with your language and framework ecosystems to implement transaction scripts in a way that's spiritually true.

Thinking about every language and framework I've worked in, there's always a canonical starting point.
I don't even consider it a hot take, though I know many people will balk at what sounds like my rejection of a very widely embraced philosophy on diversity: There is usually - at best - a handful of "advisable ways" to go about doing things. Though I don't think in practice it's as controversial as it sounds.

You've almost certainly heard the above assertion in the form of "don't fight your framework"...

When we try to apply shallow interpretations of patterns and best practices, we create what are called inner platforms. Or as some might prefer to say, "reinvent the wheel". The inherent danger in this is that each deviation from idiomatic language or framework ecosystem norms puts your broader application design into question.

As someone who has spent years toiling on a spiritual journey through various interpretations of n-tier, onion skin, hexagonal, rhomboid and trapezoidal architectures, I've found transaction scripts combined with a less dogmatic view on DRY to be liberating.

Taken in combination with vertical slicing, transaction scripts represent a kind of cognitive reset switch. Not just in your habits as a professional, but also in your navigation within any given project.

Structure and Scope

Fundamentally, the more context you can acquire at any given point in a codebase, and the speed which you can then shed it to make room while moving around; The more effective you can be and the more scope you can cut through.

I've name-dropped quite a few concepts here, each of which might seem appealing but also too abstract to simply take as-defined and be successful. But once you disabuse yourself of the shoulds and shouldn'ts you might have acquired over the last ten (or more!) years... It clicks!

What's up AI?

Imagine my surprise then, when I realized that my application of project structure norms, aligned to my framework of choice started to pay dividends as I began dabbling with having AI in my development flow!

It's not so much that I am going to try and take credit for enabling AI to work on my codebase at all. The fact is, no matter how we feel about LLMs today, both hosted and locally-runnable models have become very capable.

If you take that growing capability and treat it like another member of your project however - with all the attendant obligations to ensure you are communicating effectively - the benefits really start to stack. That's where the premise of this blog post comes from. It kicks in when you take SDLC techniques that make it easier for humans to collaborate and make them available to LLMs as well.

Rather than having to stuff my codebase up with AGENTS.md files, the existing application code serves to signpost LLMs through any choices being made.

They are, after all, excellent pattern recognizers!

Again, I'm not proposing that any of this is revolutionary, but it's reassuring to know that good fundamentals can still matter as we pass through a lot of uncertainty around what direction the industry is going in. It's nice to find ways where quality and care can in fact yield velocity.

What does it look like?

I'm probably not going to be able to explain this in a way that will address every possible angle and question. Somewhat disappointingly, this part of the post is just going to be another run-of-the-mill "how I structure my solutions in dotnet" showcase.

But yeah, you know what, what the hey:

Solution/
    AppHost/
    ServiceDefaults/
    Common.ApplicationGraph/
    Common.Domain/
    Api/
        Discovery/
        BusinessArea1/
            WireType/
            BusinessArea1EndpointExtensions.cs <- Transaction scripts
        BusinessArea2/
            WireType/
            BusinessArea2EndpointExtensions.cs <- Transaction scripts
        BusinessArea3/
            WireType/
            BusinessArea3EndpointExtensions.cs <- Transaction scripts
        CrossCuttingConcern1.cs
        CrossCuttingConcern2.cs
        Program.cs        
    Worker/
        BusinessArea1/
            BusinessArea1Workflow.cs
            BusinessArea1WorkflowActivity1.cs <- Transaction script
            BusinessArea1WorkflowActivity2.cs <- Transaction script
            BusinessArea1WorkflowActivity3.cs <- Transaction script
        BusienssArea4/
            BusinessArea4Workflow.cs
            BusinessArea4WorkflowActivity1.cs <- Transaction script
            BusinessArea4WorkflowActivity2.cs <- Transaction script
            BusinessArea4WorkflowActivity3.cs <- Transaction script
        Program.cs
    Worker.Integrations.Domain/
        ...
    Notifications/
        Program.cs
    Notifications.Domain/
        ...
    Notifications.WireTypes/
    Domain/
        Model1.cs
        Model2.cs
        Model3.cs
        Model4.cs
        DomainDbContext.cs
    Domain.Migrations/
        ...
    Domain.Test.Slow/
    Domain.Test.Fast/
    docs/
        adrs/
        conventions/

...and honestly, like you'd expect, this is continuously evolving. What you won't see in this project structure is what I refer to as n-tier slop. There are no services, no repository layers and no *Helper.cs files. Generally speaking, everything that needs to be expressed can be either by integrating with the framework or the language.

The one main thing worth calling out is that the transaction scripts are where all your control flow lives. I cannot emphasize this enough. This is something I know first-hand that many developers struggle with, as it's always possible to simply start authoring control code anywhere.

The PHP ecosystem has a saying "it's just PHP", which means if something is getting in your way, simply disregard any lifecycle you might be participating in, and go rogue.

Once you understand that each operation against your system is atomic, you stop fretting over centralizing where your logic lives or creating units of reuse. My favourite contrived example is changing a users email. There's an appreciable difference between a user changing their own email and an administrator forcing a change to it. While both those flows share a common destination - the Email property on some User entity - the code that enacts that change doesn't by-nature need to be forced through a shared pinhole.

That said, there are certain scenarios, particularly around data shape and consistency that do necessitate some centralization. And that is where your domain model comes in. By restricting the centralization to areas where scope and control flow are de-facto forbidden, you structurally enforce simplicity.

I could spend another ten pages on this and barely make a dent in "good project structure". Everything I've described is intended to promote discoverability and simplicity. The codebase should remain cognitively flat. Not only in terms of the routines, but also structurally so that it always remains approachable.

Bringing it back to AI

I'll close things out at this point as I'm hoping to keep this post medium-length. But I hope you can now see that by enforcing a structure that doesn't create ambiguity in where each concept lives, you can seed your codebase to allow both humans and LLMs to collaborate and succeed.

The only big thought I'd offer at this point is that we as an industry should explore which patterns are useful and which are dogma, driven by entrenched habits or worse, maladaptions stemming from two decades of framework antipatterns.