Four Options for a Dying Codebase — Including Palliative Care

Software Architecture Legacy Code Clean Architecture .NET Enterprise

Everyone talks about clean architecture in conference talks and blog posts. Almost nobody writes about what happens when it fails — slowly, over 8 years, across multiple vendor hand-overs, until every deployment is a gamble and developers are fleeing the project.

This is that story.

The Codebase Nobody Wanted

The system is a large .NET and Angular web application for a Swiss defense client — HR, finance, logistics, warehousing — serving thousands of users across 9 modules. It had been in development for 8 years, passed through multiple development firms, and by the time I joined as tech lead and software architect, it had a reputation problem.

Developers didn't want to work on it. It wasn't flashy. There was no greenfield excitement, no modern stack to put on your CV. Previous devs had cycled off the project one by one, each time taking institutional knowledge with them and leaving behind code that the next person didn't fully understand.

Every deployment was a roll of the dice. You'd ship a release, and bugs would surface at the customer that nobody had caught. Not because people were careless — because the codebase had become so tangled that it was nearly impossible to change one thing without breaking another. You'd fix a function and a colleague would say: "You can't do that — don't you know that breaks the background task that writes to the same tables?"

No, you didn't know. Nobody could know. There were too many unknown unknowns.

What Architecture Decay Actually Looks Like

The project had originally been structured as an onion architecture — the kind you see in textbooks, with clean layer separation and dependency rules. But over 8 years and multiple teams, the architecture had eroded from the inside.

Here's what I found when I did the diagnosis:

Business logic lived in the infrastructure layer. The onion's application layer — where business rules are supposed to live — was essentially empty. Everything had migrated outward into the infrastructure layer, where database access and business logic were tangled together in the same functions. Need to change a business rule? You're editing the DB layer. Need to change a query? Better understand the business logic wrapped around it. Need a database migration? That might require changes to business logic too.

Constructors with 10+ injected services. Services had grown into god objects. Each constructor pulled in a dozen dependencies, many doing overlapping things. The repository pattern was everywhere — necessary because of concurrent database access — but it had become a layer of indirection that made reasoning about the code harder, not easier.

Tests were theater. The infrastructure-level tests were flaky. Because they failed randomly, the team started ignoring them, then turning them off. No new tests were being written because nobody saw the value — if the existing tests couldn't be trusted, why write more? Everything depended on manual testers catching regressions before deployment. They couldn't catch everything.

Knowledge existed only in people's heads. Business rules about data scoping — which users can see which data, which roles grant which permissions — weren't represented as data types or where-clauses you could read. They were encoded in code logic that you had to trace through to understand. When those people left the project, the knowledge left with them.

The technical term for all of this is architecture erosion. But experiencing it feels more like walking through a building where every room has been renovated by a different contractor who didn't talk to the others, and now nothing connects properly but the lights still turn on — most of the time.

The Root Cause

The architecture had failed not because onion architecture is wrong, but because nobody enforced the rules. Without someone actively defending layer boundaries, developers take shortcuts. A quick DB call here, a business rule mixed into a repository there. Each individual shortcut is small. Over 8 years, thousands of small shortcuts compound into a codebase where the architecture diagram bears no resemblance to the actual code.

The deeper root cause was organizational: the project had no technical owner who stayed long enough to care. Vendor hand-overs meant that each new team inherited a mess, added features on top, and passed it along. The customer kept requesting features, those got built, and nobody had the mandate or the time to say "we need to stop and fix the foundation first."

The Four Options

After spending weeks analyzing the codebase, I wrote an architecture assessment and presented four possible paths forward:

Option 1: Strangler-Fig via Controller Splitting

Refactor the C# backend by splitting bloated controllers into clean, vertical-slice modules — one by one, behind the existing API surface. Low infrastructure risk, but slow and requires discipline across the whole team to maintain two patterns side by side.

Option 2: Strangler-Fig via API Gateway

Put a reverse proxy in front of the application. New requests get routed to a new backend built with vertical-slice architecture. Legacy requests go to the old backend. Over time, you migrate module by module until the old backend is empty. More infrastructure setup upfront — the whole app needs to be containerized first — but cleaner separation between old and new.

Option 3: Test-First Stabilization

Stop all feature work. Write tests until the codebase is stable enough that deployments stop producing surprise bugs. Doesn't fix the architecture, but stops the bleeding.

Option 4: Palliative Care

Keep doing what we're currently doing. Accept new feature requests from the customer, implement them in the existing codebase, and watch the problems get bigger with each release. I called this option Palliativbegleitung — palliative care.

I included option 4 deliberately. It was provocative, and it was meant to be. When you're neck-deep in a legacy project, it's easy to normalize the dysfunction — "it's always been like this, it's fine, we manage." Naming the status quo as palliative care forced an honest conversation about where the project was heading if nothing changed.

It landed as a wake-up call. The room got serious.

What Happened Next

I presented the options in a PowerPoint that lived on Confluence. The team discussed, debated, weighed the trade-offs. Then something unexpected happened: the customer found the presentation.

The customer's response was direct. They didn't like option 4 — nobody likes being told their multi-million-franc project is on life support. But more importantly, they confirmed that this project was funded until 2034. The modernization had a budget.

The team chose option 2 — Strangler-Fig via API Gateway. The first step was containerizing the entire application with Docker so the reverse proxy could be deployed cleanly alongside the legacy backend. The new backend would use vertical-slice architecture with MediatR and clean separation of concerns.

I also rewrote the CI/CD pipeline in Nuke and cut 20 minutes off the test suite runtime — a concrete improvement that gave the team a small, immediate win alongside the longer-term modernization roadmap.

The most important impact wasn't technical, though. It was morale. Giving the team a clear direction — here's what's wrong, here's how we fix it, here's the roadmap — turned a project nobody wanted to work on into one that felt like it had a future. People stopped dreading sprint planning.

I eventually moved on to other projects within the same organization. The team continued the modernization work. The Docker containerization, the API gateway, the vertical-slice migration — they're executing the strategy that came out of that assessment.

What I Learned

Architecture doesn't decay because of bad developers. Every developer who worked on this codebase was competent. The decay happened because of turnover, time pressure, and the absence of someone defending architectural boundaries over the long term. Vendor hand-overs are particularly destructive — each new team optimizes for delivery speed, not structural integrity.

Name the status quo honestly. The Palliativbegleitung option worked because it gave the dysfunction a name. As long as the current approach was just "how things are," nobody felt urgency to change. Labeling it as palliative care made the trajectory undeniable.

Architecture assessments are undervalued. Spending a few weeks doing nothing but reading code, drawing diagrams, and writing down what's broken — this felt slow and unproductive in the moment. But the resulting document became the foundation for a multi-year roadmap and secured significant funding. The highest-leverage work I did on this project wasn't writing code. It was writing an honest assessment.

You don't always get credit, and that's okay. If the codebase gets better and the team is in a better place, the architecture work succeeded — regardless of whether anyone remembers who proposed it. The impact matters more than the attribution.