Custom apps8 min read

One Process. Forty Clients. Shared Crashes.

A twenty-year .NET monolith decomposed so document work, workflows, and tenant traffic could fail independently.

Vivek Chandramohan

Vivek Chandramohan

Published 12 September 2026

.NET monolith to microservices case study

A B2B document platform had carried the business for twenty years as a single ASP.NET monolith: intake, workflows, and document processing in one process, one SQL Server, one IIS pool. It was solid. Then it had to house forty clients. Friday-night PDF batches deadlocked the database and took every tenant’s portal down with them. We, at Futluz, split the monolith into services so document work, workflow, and interactive traffic could fail independently—and the platform could hold 99.9% uptime with more than one neighbour on the box.


TL;DR

Problem: ASP.NET Framework 4.8 monolith, 20 years in production, one SQL database, document conversion and age-old workflows in the same w3wp process. Deadlocks under batch load. Multi-tenant, shared fate.

Approach: Strangler extraction, not a rewrite. Document processing and workflow left the web process first, onto queues and worker services. Tenant-aware ASP.NET Core APIs next. The monolith shrank until it was a BFF, then it was retired.

Scale & results: Forty enterprise clients, ~2.1 million documents a month. Fourteen months. Portal remaining up while batches ran. Measured availability from 99.1% to 99.94% over two consecutive quarters. A noisy tenant no longer took the estate down.

Why it worked: The monolith had earned twenty years. Microservices were the answer to isolation and load shape, not a fashion. We extracted the two things that killed the process—documents and long workflows—before we touched the rest.

The application was not the problem. Putting forty clients and a PDF farm in the same process was the problem. Once documents left the web server, the outages stopped looking like destiny.

Ro***, ********

CTO, Platform

The client challenge

The product started in 2005 as a custom ASP.NET application for one professional-services firm: receive documents, run a workflow, file the result. Twenty years later it was a hosted platform for forty enterprise clients—legal, insurance, and corporate operations teams—processing about 2.1 million documents a month. The stack had grown in place: ASP.NET Framework 4.8, a 1.4-million-line solution, one IIS application pool, one SQL Server with 4.8 TB of tenant data in shared tables, and a Windows Service that had been folded back into the web process so operations would only have one thing to restart.

That last decision is the one that aged worst. PDF conversion, OCR hand-off, and large-file hashing ran in-process. A client with a Friday batch of 40,000 files would pin CPU, hold SQL locks across the workflow tables, and deadlock check-in of documents that other tenants were trying to open in the portal. The UI thread pool starved. Health checks failed. Load balancers pulled the node. Every client saw the outage, including the ones who had uploaded nothing.

Workflows were the other load-bearing accident. Status machines for intake, review, exception, and archive had accreted as stored procedures and static classes named for clients who had left in 2012. Changing a step for one tenant meant a full regression of the monolith. Releases were monthly because a deadlock “fix” in invoicing could stall document check-in. Measured availability sat around 99.1%—fine for an internal tool, not for a platform that had to house forty paying clients and still look like 99.9%.

The application had been solid for twenty years because it had been, for most of that time, one client’s system. Multi-tenancy without isolation turned a reliable monolith into a shared crash domain. That is the whole case.

Why not keep scaling the box

Bigger SQL, more IIS nodes, and a bigger VM are the first three proposals in every steering pack. We costed them so they could be rejected with numbers.

  1. Scale the monolith vertically: Already done twice. Document CPU and SQL lock duration grow with the noisiest tenant. A larger box makes the blast radius more expensive, not smaller.
  2. Add read replicas and more web nodes: Helps portal GETs. Does not help a writer that converts PDFs and holds workflow rows in the same transaction. Deadlocks moved with the primary.
  3. Rewrite the product as greenfield microservices: Attractive in a slide. Fourteen months of no feature delivery, a dual-run of 4.8 TB, and a cutover that would have to be right first time for forty clients. Rejected.
  4. Extract the failure domains, leave the rest until it is small: Selected. Documents and workflow out of the web process first. Tenant-aware APIs second. The monolith becomes a façade, then it goes.

Our solution

  1. Name the seams before drawing services
    • We instrumented production for six weeks: which endpoints held SQL longest, which jobs allocated CPU, which stored procedures deadlocked, which tenants correlated with incidents. Two domains produced 80% of the downtime: document processing and long-running workflow transitions. Everything else was noisy and survivable.
    • Service boundaries followed those domains, not the solution folders. “Shared kernel” libraries that every project referenced were treated as a risk, not as an asset.
  2. Documents leave the web process
    • Conversion, hashing, thumbnailing, and virus scan moved to a pool of .NET 8 worker services behind a queue. The portal accepted the upload, wrote a blob, published a message, and returned. CPU spikes stayed on workers that could scale independently and die without taking IIS with them.
    • Poison messages and oversized files had a dead-letter path. A bad PDF from one tenant no longer recycled the application pool for everyone else.
  3. Workflow as a service, not a stored procedure farm
    • Age-old status machines were modelled as explicit workflow definitions per tenant, executed by a dedicated service with its own database. Steps that used to run inside a single SQL transaction became commands with timeouts, retries, and compensation. A stuck review queue for Client 17 no longer locked Client 3’s check-in table.
    • Client-specific branches that had been copy-pasted through the monolith were collapsed into configuration. The ones that were genuinely different stayed as tenant extensions, isolated, testable, and deletable.
  4. Tenant-aware APIs on ASP.NET Core
    • Interactive traffic moved to ASP.NET Core 8 APIs with tenant context on every request, connection resiliency, and query patterns that stopped scanning forty clients to serve one. Read models for the portal were separated from the write models the workers used.
    • Identity, billing, and notification were extracted only after documents and workflow were stable. Three services that earned their keep beat twelve that existed on an architecture diagram.
  5. Data: isolate what contended, not everything on day one
    • Workflow and document metadata left the shared monolith database first. The remaining operational data stayed on SQL Server with tenant keys, row-level filters, and deadlock-priority hygiene until traffic justified further splits. We did not start with forty databases. We started with the two that deadlocked.
  6. Contract tests and a strangler façade
    • The old IIS app kept serving routes that had not moved. A gateway sent document and workflow traffic to the new services. Each extracted endpoint had contract tests against the legacy behaviour so a tenant could be flipped without a platform-wide cutover.
    • The last monolith deploy removed the in-process converters and the static workflow classes. What remained was retired three months later.

Fourteen months, one platform

Peak team of nineteen: six .NET engineers who already knew the monolith, three who did not—on purpose—four QA, a data engineer, an SRE, and programme management sitting with the client’s operations lead. The people who had kept the monolith alive were not interviewed once and replaced. They owned the first two extractions, because they knew which stored procedure actually deadlocked on Friday.

QA stopped treating “the site is up” as the test. Suites ran a noisy-tenant document flood against a quiet tenant’s portal session and failed if either starved. Chaos days killed a worker pool in staging and expected the UI to stay up. Two of those days found missing timeouts. Both were fixed before any tenant flipped.

Programme management held the line microservices programmes usually lose: no new product features in the extracted domains until the old process no longer processed documents. A workflow redesign for a flagship client waited until that client was off the monolith path. Isolation first, then speed.


The results

  • Availability: 99.1% to 99.94% over two consecutive quarters. Portal remaining up while document batches ran. On track against a 99.9% commercial target.
  • Isolation: A noisy tenant’s Friday batch no longer deadlocks other clients’ check-in. Worker failure is a delayed document, not a downed platform.
  • Capacity: Document workers scale independently of the API. Peak conversion no longer requires oversizing the web tier for forty tenants.
  • Workflows: Tenant definitions instead of 2012 static classes. A step change for one client ships without a monolith regression.
  • Estate: In-process converters gone. Monolith retired three months after the last route moved. 1.4 million lines reduced to services with explicit contracts.
  • Multi-tenancy: Forty clients on one platform without a shared crash domain. New tenants onboard onto APIs and workers, not onto a larger IIS pool.

What changed for the people using it

Operations no longer restart IIS on Friday night “just in case.” Support no longer tells Client 12 that Client 7’s batch is why their portal is slow. Engineers change a workflow without loading the entire solution. The twenty-year product is still the product. It is no longer one process pretending to be a platform.

Business impact

Risk: Off a shared crash domain and deadlock-driven outages, onto services that can fail in pieces and still serve forty clients.

Cost: Web-tier oversizing for document CPU came out. Incident hours spent on “the site is down for everyone” dropped with the blast radius.

Speed: Tenant workflow changes ship on a service release train. Document capacity scales without a portal deploy.


A monolith-to-microservices move is not a verdict on the twenty-year application. It is a judgement that one process cannot convert documents, run old workflows, and house forty clients without sharing their failures. We extract the failure domains first; 99.9% is what you measure after isolation is real, not what you put on the architecture slide.

 

Custom apps.NETMicroservices