How I used AI to restructure a real .NET codebase without turning it into microservices, over-engineering the architecture, or trusting AI blindly.

My .NET API project gradually became the place where everything lived.

Endpoints. Business logic. EF Core models. Worker dependencies. Tests. Service registration.

It started as a straightforward vertical-slice architecture, which was perfectly fine at first. But over time, the Server project grew to be the centre of the entire application.

So I refactored it into a modular monolith.

Not microservices. Not a rewrite. Not architecture theatre.

Just stronger boundaries within the same application.

The hard part was not moving the code.

The hard part was using AI to move quickly without it quietly redesigning the system for me.

I began thinking of the process as REFACTOR: a simple loop that uses AI as an accelerator while maintaining architectural judgment with the developer.

What the Codebase does

The project is a .NET application for image recognition workflows and AI reporting.

At a high level, it has an API, a background worker, EF Core persistence, database migrations, frontend apps, integration tests, and Aspire orchestration.

Aspire plays a crucial role here. It consolidates the application locally: API, worker, database, storage, frontend resources, migrations, and dependent services.

During the refactor, that meant I was not just moving files and hoping the solution compiled. I was able to run the entire application and verify it still performed as a single system.

How I ended up here

The original structure used vertical slices within the API project.

That was a good starting point. For a small product, feature folders are simple and effective. You can keep the endpoint, request, validator, handler, workflow, and persistence code close together.

The issue wasn’t with vertical slices.

It was that all slices existed inside a single host project.

Over time, the API project accumulated too many responsibilities:

  • hosting the web pipeline
  • containing feature code
  • including EF Core models
  • registering business services
  • exposing elements the worker required
  • acting as the point where tests accessed almost everything

The core problem was ownership.

Although the code was arranged into folders, the project boundary didn’t protect the modules from one another.

The Smell

I realised the structure was beginning to strain when simple changes kept crossing unseen boundaries.

A worker change required something from the API project.

A test accessed the Server because that was where everything was stored.

A feature needed another feature’s EF model because it was nearby and easy to reference.

None of these issues were disasters on their own. But collectively, they served as a warning.

The folders looked tidy, but the dependencies told a different story.

The Refactor

The old structure was roughly:

Server
Features
ImageAnalysis
AiUsage
DevTools
Persistence
Program.cs
Worker
Migrations
Tests

The new structure is closer to:

src/
AppHost
Server
Worker
Migrations
ServiceDefaults
Modules.ImageAnalysis
Modules.AiUsage
Modules.DevTools
tests/
Modules.ImageAnalysis.Tests
Modules.AiUsage.Tests
Server.Tests
IntegrationTests

Here is the change at a higher level:

Before and after modular monolith refactor diagram

The key change was not the number of folders.

It was where ownership resided.

Each real module now owns its endpoints, use cases, services, EF Core entities, DbContext, EF configuration, migration setup, public contracts, and module-specific tests.

The Server is now the API composition root. The Worker references the module it needs instead of referencing the API project. The Migrations host manages the module DbContexts.

This remains a vertical slice architecture, but within stronger module boundaries.

Why I did not move to Microservices

This didn’t require microservices.

The modules are not separate deployable services; they are class libraries within a single modular monolith.

The application still uses one physical database and follows the same Aspire application model. The runtime complexity didn’t increase just because the code boundaries became clearer.

That was intentional.

I aimed for better modularity, not the complexity of a distributed system.

EF Core boundaries

One of the biggest decisions was what to do with EF Core entities.

I did not move every entity into a shared persistence project.

That would have looked tidy, but it would have created the wrong kind of coupling. In a modular monolith, each module should own its own data model.

So, each real module got its own DbContext.

The modules can still use the same physical database and connection string, but each module owns its schema and migration history. Other modules should not casually import another module’s EF entities or query another module’s DbSet.

Cross-module behaviour should go through small public contracts or application services.

A simple rule: if another module needs your data, expose a contract, not your persistence model.

How AI helped

This part I found most interesting.

I didn’t ask AI to “refactor my repo” and blindly accept the result.

I used AI as a research assistant, architecture critic, implementation partner, and quick pair programmer.

I used AI in layers rather than giving it one big instruction:

  1. Ask AI to inspect the current repo.
  2. Ask whether the existing structure truly followed the principles of a modular monolith.
  3. Use multiple AI research agents to compare modular monolith patterns, EF Core boundaries, migrations, and test structures.
  4. Ask AI to summarise the findings.
  5. Adapt that summary to my actual project.
  6. Push back on anything too complex.
  7. Turn the decision into a concrete implementation plan.
  8. Have AI implement the plan.
  9. Validate the outcome against the real application, not just the AI summary.

The research step was helpful. It assisted in comparing how people structure modular monoliths and where the typical boundaries are.

But the most valuable part was applying that research to the actual codebase.

The REFACTOR Approach

Looking back, the process that worked was not “ask AI to redesign the application”.

It was REFACTOR:

  • R - Review the current structure
  • E - Explore better architecture options
  • F - Find the real ownership boundaries
  • A - Avoid premature microservices
  • C - Choose the boring version
  • T - Test through real validation gates
  • O - Observe where AI overreaches
  • R - Refine until the repo proves it works

The order mattered. If I jumped straight from Explore to Implement, AI produced a cleaner-looking design than the project actually needed. The useful step was Choose: deliberately choosing the boring version before letting AI touch the code.

That was the useful loop: let AI accelerate the work, but keep the architecture boring, explicit, and validated.

REFACTOR loop diagram

Does AI gaslight you?

Not intentionally. However, it can make a codebase seem more polished than it truly is.

AI initially over-engineered parts of the plan, favouring more separation, more contracts, and more ceremony than the project required.

It also overlooked some details. It dropped parts of the plan, particularly around test structure. Corrections were needed regarding project references, endpoint discovery, Aspire service defaults, migrations, worker startup, frontend paths, and resolving conflicts after package upgrades.

The pattern is subtle. AI usually does not fail by stating “I ignored your architecture.” It fails by giving the impression that the architecture remains intact when only the happy path has been updated.

That is why I treat AI output as a quick draft, not a definitive source.

The source of truth is still:

  • the repo
  • the diff
  • the compiler
  • the tests
  • the running application
  • the architecture rules written into docs

When AI claims “this is done”, I still ask: does it build? Do the tests pass? Does Aspire start up? Did migrations run? Does the worker resolve dependencies? Did one module accidentally start relying on another module’s internals?

AI can be useful and wrong at the same time.

The Guardrails that mattered

The important part was not asking AI to move code.

The real focus was on enforcing the refactor through validation gates.

For this refactor, that involved verifying:

  • Does the solution build?
  • Do the module tests pass?
  • Do server tests still focus on API composition?
  • Do integration tests continue to cover real workflows?
  • Does Aspire launch the full application?
  • Do migrations run successfully?
  • Does the worker start without referencing the API project?
  • Are module boundaries correctly reflected in test project references?
  • Did any module unintentionally depend on another module’s EF entities?

AI can move code quickly. Guardrails ensure that movement remains safe.

What I gained

The biggest win was achieving clarity.

Before the refactor, the API project was the host, feature container, persistence container, and dependency hub.

After the refactor, the solution structure explains the system.

I also gained better test boundaries. Module tests now reference modules directly. Server tests focus on API composition. Integration tests still cover real workflows, but cross-module behaviour is now explicit.

The worker also became cleaner, referencing only the module it needs rather than the API host.

Additionally, ownership of EF Core is now much easier to understand. Each module owns its own DbContext, entities, configurations, and migrations.

The Productivity gain

I am not going to make up a fake percentage.

The gain was the speed of iteration.

AI could inspect files, propose structure, move code, update project references, fix compiler errors, adjust tests, update documentation, and summarize the diff quickly.

But the productivity came from pairing with AI, not delegating architecture ownership to it.

Why should anyone care?

This is not advice for every codebase. If your app remains small and the boundaries are clear, feature folders may be enough.

But a lot of teams eventually find themselves in a middle ground.

They neither require microservices nor a massive Clean Architecture template. Yet, they can still experience the frustration of one API project gradually turning into the entire application.

A modular monolith provides clearer boundaries without the complexity of distributed systems.

And AI can assist you in reaching that goal more quickly, as long as you validate everything it suggests.

Conclusion

This refactor succeeded because I didn’t treat AI as the architect.

I regarded it as a quick pair programmer without ultimate authority.

It could research, compare options, move code, update references, fix compiler errors, and summarise changes faster than I could do all manually.

But the architectural judgment still needed to come from me.

The useful pattern was not:

AI, redesign my application.

It was:

AI, help me explore the options, choose the boring version, implement it in small steps, and prove that it still works.

That is the version of AI-assisted engineering I trust: not magic, not delegation, but acceleration with guardrails.